aboutsummaryrefslogtreecommitdiffstats
path: root/java/src
diff options
context:
space:
mode:
Diffstat (limited to 'java/src')
-rw-r--r--java/src/com/android/inputmethod/compat/IntentCompatUtils.java9
-rw-r--r--java/src/com/android/inputmethod/dictionarypack/ButtonSwitcher.java15
-rw-r--r--java/src/com/android/inputmethod/dictionarypack/DictionaryListInterfaceState.java16
-rw-r--r--java/src/com/android/inputmethod/dictionarypack/DictionarySettingsFragment.java18
-rw-r--r--java/src/com/android/inputmethod/dictionarypack/LocaleUtils.java2
-rw-r--r--java/src/com/android/inputmethod/dictionarypack/MetadataDbHelper.java26
-rw-r--r--java/src/com/android/inputmethod/dictionarypack/WordListPreference.java20
-rw-r--r--java/src/com/android/inputmethod/keyboard/Key.java14
-rw-r--r--java/src/com/android/inputmethod/keyboard/KeyboardId.java2
-rw-r--r--java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java2
-rw-r--r--java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java62
-rw-r--r--java/src/com/android/inputmethod/keyboard/MainKeyboardView.java75
-rw-r--r--java/src/com/android/inputmethod/keyboard/PointerTracker.java25
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/GestureStrokeWithPreviewPoints.java23
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/GestureTrail.java64
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/HermiteInterpolator.java5
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java82
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/KeyStyle.java4
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/KeyboardCodesSet.java3
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/KeyboardState.java48
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/MatrixUtils.java166
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/SmoothingUtils.java102
-rw-r--r--java/src/com/android/inputmethod/latin/AdditionalFeaturesSettingUtils.java47
-rw-r--r--java/src/com/android/inputmethod/latin/AdditionalSubtype.java12
-rw-r--r--java/src/com/android/inputmethod/latin/AssetFileAddress.java2
-rw-r--r--java/src/com/android/inputmethod/latin/AutoCorrection.java14
-rw-r--r--java/src/com/android/inputmethod/latin/BinaryDictionary.java13
-rw-r--r--java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java49
-rw-r--r--java/src/com/android/inputmethod/latin/Constants.java32
-rw-r--r--java/src/com/android/inputmethod/latin/DictionaryFactory.java23
-rw-r--r--java/src/com/android/inputmethod/latin/DictionaryInfoUtils.java11
-rw-r--r--java/src/com/android/inputmethod/latin/InputAttributes.java2
-rw-r--r--java/src/com/android/inputmethod/latin/LatinIME.java54
-rw-r--r--java/src/com/android/inputmethod/latin/LocaleUtils.java2
-rw-r--r--java/src/com/android/inputmethod/latin/NativeSuggestOptions.java53
-rw-r--r--java/src/com/android/inputmethod/latin/ResourceUtils.java81
-rw-r--r--java/src/com/android/inputmethod/latin/SeekBarDialogPreference.java11
-rw-r--r--java/src/com/android/inputmethod/latin/Settings.java8
-rw-r--r--java/src/com/android/inputmethod/latin/SettingsFragment.java17
-rw-r--r--java/src/com/android/inputmethod/latin/SettingsValues.java8
-rw-r--r--java/src/com/android/inputmethod/latin/StringUtils.java90
-rw-r--r--java/src/com/android/inputmethod/latin/Suggest.java12
-rw-r--r--java/src/com/android/inputmethod/latin/SuggestedWords.java20
-rw-r--r--java/src/com/android/inputmethod/latin/Utils.java126
-rw-r--r--java/src/com/android/inputmethod/latin/personalization/AccountUtils.java19
-rw-r--r--java/src/com/android/inputmethod/latin/setup/LauncherIconVisibilityManager.java2
-rw-r--r--java/src/com/android/inputmethod/latin/suggestions/MoreSuggestions.java113
-rw-r--r--java/src/com/android/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java581
-rw-r--r--java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java546
-rw-r--r--java/src/com/android/inputmethod/latin/utils/Base64Reader.java117
-rw-r--r--java/src/com/android/inputmethod/latin/utils/CsvUtils.java319
-rw-r--r--java/src/com/android/inputmethod/research/FeedbackLog.java32
-rw-r--r--java/src/com/android/inputmethod/research/LogUnit.java14
-rw-r--r--java/src/com/android/inputmethod/research/MainLogBuffer.java72
-rw-r--r--java/src/com/android/inputmethod/research/ResearchLog.java11
-rw-r--r--java/src/com/android/inputmethod/research/ResearchLogger.java253
-rw-r--r--java/src/com/android/inputmethod/research/Statistics.java44
-rw-r--r--java/src/com/android/inputmethod/research/Uploader.java4
58 files changed, 2456 insertions, 1141 deletions
diff --git a/java/src/com/android/inputmethod/compat/IntentCompatUtils.java b/java/src/com/android/inputmethod/compat/IntentCompatUtils.java
index df2e22fe8..965a2a891 100644
--- a/java/src/com/android/inputmethod/compat/IntentCompatUtils.java
+++ b/java/src/com/android/inputmethod/compat/IntentCompatUtils.java
@@ -21,16 +21,15 @@ import android.content.Intent;
public final class IntentCompatUtils {
// Note that Intent.ACTION_USER_INITIALIZE have been introduced in API level 17
// (Build.VERSION_CODE.JELLY_BEAN_MR1).
- public static final String ACTION_USER_INITIALIZE =
- (String)CompatUtils.getFieldValue(null, null,
+ private static final String ACTION_USER_INITIALIZE =
+ (String)CompatUtils.getFieldValue(null /* receiver */, null /* defaultValue */,
CompatUtils.getField(Intent.class, "ACTION_USER_INITIALIZE"));
private IntentCompatUtils() {
// This utility class is not publicly instantiable.
}
- public static boolean has_ACTION_USER_INITIALIZE(final Intent intent) {
- return ACTION_USER_INITIALIZE != null && intent != null
- && ACTION_USER_INITIALIZE.equals(intent.getAction());
+ public static boolean is_ACTION_USER_INITIALIZE(final String action) {
+ return ACTION_USER_INITIALIZE != null && ACTION_USER_INITIALIZE.equals(action);
}
}
diff --git a/java/src/com/android/inputmethod/dictionarypack/ButtonSwitcher.java b/java/src/com/android/inputmethod/dictionarypack/ButtonSwitcher.java
index 5ab94a429..c5aca174a 100644
--- a/java/src/com/android/inputmethod/dictionarypack/ButtonSwitcher.java
+++ b/java/src/com/android/inputmethod/dictionarypack/ButtonSwitcher.java
@@ -57,6 +57,11 @@ public class ButtonSwitcher extends FrameLayout {
super(context, attrs, defStyle);
}
+ public void reset() {
+ mStatus = NOT_INITIALIZED;
+ mAnimateToStatus = NOT_INITIALIZED;
+ }
+
@Override
protected void onLayout(final boolean changed, final int left, final int top, final int right,
final int bottom) {
@@ -64,9 +69,7 @@ public class ButtonSwitcher extends FrameLayout {
mInstallButton = (Button)findViewById(R.id.dict_install_button);
mCancelButton = (Button)findViewById(R.id.dict_cancel_button);
mDeleteButton = (Button)findViewById(R.id.dict_delete_button);
- mInstallButton.setOnClickListener(mOnClickListener);
- mCancelButton.setOnClickListener(mOnClickListener);
- mDeleteButton.setOnClickListener(mOnClickListener);
+ setInternalOnClickListener(mOnClickListener);
setButtonPositionWithoutAnimation(mStatus);
if (mAnimateToStatus != NOT_INITIALIZED) {
// We have been asked to animate before we were ready, so we took a note of it.
@@ -139,6 +142,12 @@ public class ButtonSwitcher extends FrameLayout {
public void setInternalOnClickListener(final OnClickListener listener) {
mOnClickListener = listener;
+ if (null != mInstallButton) {
+ // Already laid out : do it now
+ mInstallButton.setOnClickListener(mOnClickListener);
+ mCancelButton.setOnClickListener(mOnClickListener);
+ mDeleteButton.setOnClickListener(mOnClickListener);
+ }
}
private ViewPropertyAnimator animateButton(final View button, final int direction) {
diff --git a/java/src/com/android/inputmethod/dictionarypack/DictionaryListInterfaceState.java b/java/src/com/android/inputmethod/dictionarypack/DictionaryListInterfaceState.java
index de3711c27..5ad5900d4 100644
--- a/java/src/com/android/inputmethod/dictionarypack/DictionaryListInterfaceState.java
+++ b/java/src/com/android/inputmethod/dictionarypack/DictionaryListInterfaceState.java
@@ -16,8 +16,11 @@
package com.android.inputmethod.dictionarypack;
+import android.view.View;
+
import com.android.inputmethod.latin.CollectionUtils;
+import java.util.ArrayList;
import java.util.HashMap;
/**
@@ -37,6 +40,7 @@ public class DictionaryListInterfaceState {
}
private HashMap<String, State> mWordlistToState = CollectionUtils.newHashMap();
+ private ArrayList<View> mViewCache = CollectionUtils.newArrayList();
public boolean isOpen(final String wordlistId) {
final State state = mWordlistToState.get(wordlistId);
@@ -64,4 +68,16 @@ public class DictionaryListInterfaceState {
state.mOpen = false;
}
}
+
+ public View findFirstOrphanedView() {
+ for (final View v : mViewCache) {
+ if (null == v.getParent()) return v;
+ }
+ return null;
+ }
+
+ public View addToCacheAndReturnView(final View view) {
+ mViewCache.add(view);
+ return view;
+ }
}
diff --git a/java/src/com/android/inputmethod/dictionarypack/DictionarySettingsFragment.java b/java/src/com/android/inputmethod/dictionarypack/DictionarySettingsFragment.java
index 618322357..4b89d20bb 100644
--- a/java/src/com/android/inputmethod/dictionarypack/DictionarySettingsFragment.java
+++ b/java/src/com/android/inputmethod/dictionarypack/DictionarySettingsFragment.java
@@ -222,7 +222,9 @@ public final class DictionarySettingsFragment extends PreferenceFragment
refreshNetworkState();
removeAnyDictSettings(prefScreen);
+ int i = 0;
for (Preference preference : prefList) {
+ preference.setOrder(i++);
prefScreen.addPreference(preference);
}
}
@@ -302,7 +304,7 @@ public final class DictionarySettingsFragment extends PreferenceFragment
// the description.
final String key = matchLevelString + "." + description + "." + wordlistId;
final WordListPreference existingPref = prefMap.get(key);
- if (null == existingPref || hasPriority(status, existingPref.mStatus)) {
+ if (null == existingPref || existingPref.hasPriorityOver(status)) {
final WordListPreference oldPreference = mCurrentPreferenceMap.get(key);
final WordListPreference pref;
if (null != oldPreference
@@ -313,7 +315,7 @@ public final class DictionarySettingsFragment extends PreferenceFragment
// need to be the same, others have been tested through the key of the
// map. Also, status may differ so we don't want to use #equals() here.
pref = oldPreference;
- pref.mStatus = status;
+ pref.setStatus(status);
} else {
// Otherwise, discard it and create a new one instead.
pref = new WordListPreference(activity, mDictionaryListInterfaceState,
@@ -329,18 +331,6 @@ public final class DictionarySettingsFragment extends PreferenceFragment
}
}
- /**
- * Finds out if a given status has priority over another for display order.
- *
- * @param newStatus
- * @param oldStatus
- * @return whether newStatus has priority over oldStatus.
- */
- private static boolean hasPriority(final int newStatus, final int oldStatus) {
- // Both of these should be one of MetadataDbHelper.STATUS_*
- return newStatus > oldStatus;
- }
-
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
switch (item.getItemId()) {
diff --git a/java/src/com/android/inputmethod/dictionarypack/LocaleUtils.java b/java/src/com/android/inputmethod/dictionarypack/LocaleUtils.java
index d0e8446f5..77f67b8a3 100644
--- a/java/src/com/android/inputmethod/dictionarypack/LocaleUtils.java
+++ b/java/src/com/android/inputmethod/dictionarypack/LocaleUtils.java
@@ -144,7 +144,7 @@ public final class LocaleUtils {
public static String getMatchLevelSortedString(final int matchLevel) {
// This works because the match levels are 0~99 (actually 0~30)
// Ideally this should use a number of digits equals to the 1og10 of the greater matchLevel
- return String.format("%02d", MATCH_LEVEL_MAX - matchLevel);
+ return String.format(Locale.ROOT, "%02d", MATCH_LEVEL_MAX - matchLevel);
}
/**
diff --git a/java/src/com/android/inputmethod/dictionarypack/MetadataDbHelper.java b/java/src/com/android/inputmethod/dictionarypack/MetadataDbHelper.java
index 03ed267c3..1511dbcfe 100644
--- a/java/src/com/android/inputmethod/dictionarypack/MetadataDbHelper.java
+++ b/java/src/com/android/inputmethod/dictionarypack/MetadataDbHelper.java
@@ -572,7 +572,8 @@ public class MetadataDbHelper extends SQLiteOpenHelper {
* If several clients use the same metadata URL, we know to only download it once, and
* dispatch the update process across all relevant clients when the download ends. This means
* several clients may share a single download ID if they share a metadata URI.
- * The dispatching is done in {@link UpdateHandler#downloadFinished(Context, Intent)}, which
+ * The dispatching is done in
+ * {@link UpdateHandler#downloadFinished(Context, android.content.Intent)}, which
* finds out about the list of relevant clients by calling this method.
*
* @param context a context instance to open the databases
@@ -863,17 +864,20 @@ public class MetadataDbHelper extends SQLiteOpenHelper {
r.getAsString(WORDLISTID_COLUMN),
Integer.toString(STATUS_INSTALLED) },
null, null, null);
- if (c.moveToFirst()) {
- // There should never be more than one file, but if there are, it's a bug
- // and we should remove them all. I think it might happen if the power of the
- // phone is suddenly cut during an update.
- final int filenameIndex = c.getColumnIndex(LOCAL_FILENAME_COLUMN);
- do {
- Utils.l("Setting for removal", c.getString(filenameIndex));
- filenames.add(c.getString(filenameIndex));
- } while (c.moveToNext());
+ try {
+ if (c.moveToFirst()) {
+ // There should never be more than one file, but if there are, it's a bug
+ // and we should remove them all. I think it might happen if the power of
+ // the phone is suddenly cut during an update.
+ final int filenameIndex = c.getColumnIndex(LOCAL_FILENAME_COLUMN);
+ do {
+ Utils.l("Setting for removal", c.getString(filenameIndex));
+ filenames.add(c.getString(filenameIndex));
+ } while (c.moveToNext());
+ }
+ } finally {
+ c.close();
}
-
r.put(STATUS_COLUMN, STATUS_INSTALLED);
db.beginTransactionNonExclusive();
// Delete all old entries. There should never be any stalled entries, but if
diff --git a/java/src/com/android/inputmethod/dictionarypack/WordListPreference.java b/java/src/com/android/inputmethod/dictionarypack/WordListPreference.java
index 451a0fb82..7ec7e9c13 100644
--- a/java/src/com/android/inputmethod/dictionarypack/WordListPreference.java
+++ b/java/src/com/android/inputmethod/dictionarypack/WordListPreference.java
@@ -61,7 +61,7 @@ public final class WordListPreference extends Preference {
public final Locale mLocale;
public final String mDescription;
// The status
- public int mStatus;
+ private int mStatus;
// The size of the dictionary file
private final int mFilesize;
@@ -92,12 +92,25 @@ public final class WordListPreference extends Preference {
setKey(wordlistId);
}
- private void setStatus(final int status) {
+ public void setStatus(final int status) {
if (status == mStatus) return;
mStatus = status;
setSummary(getSummary(status));
}
+ @Override
+ public View onCreateView(final ViewGroup parent) {
+ final View orphanedView = mInterfaceState.findFirstOrphanedView();
+ if (null != orphanedView) return orphanedView; // Will be sent to onBindView
+ final View newView = super.onCreateView(parent);
+ return mInterfaceState.addToCacheAndReturnView(newView);
+ }
+
+ public boolean hasPriorityOver(final int otherPrefStatus) {
+ // Both of these should be one of MetadataDbHelper.STATUS_*
+ return mStatus > otherPrefStatus;
+ }
+
private String getSummary(final int status) {
switch (status) {
// If we are deleting the word list, for the user it's like it's already deleted.
@@ -209,6 +222,9 @@ public final class WordListPreference extends Preference {
final ButtonSwitcher buttonSwitcher =
(ButtonSwitcher)view.findViewById(R.id.wordlist_button_switcher);
+ // We need to clear the state of the button switcher, because we reuse views; if we didn't
+ // reset it would animate from whatever its old state was.
+ buttonSwitcher.reset();
if (mInterfaceState.isOpen(mWordlistId)) {
// The button is open.
final int previousStatus = mInterfaceState.getStatus(mWordlistId);
diff --git a/java/src/com/android/inputmethod/keyboard/Key.java b/java/src/com/android/inputmethod/keyboard/Key.java
index 1550e77e3..4ef8653f6 100644
--- a/java/src/com/android/inputmethod/keyboard/Key.java
+++ b/java/src/com/android/inputmethod/keyboard/Key.java
@@ -113,12 +113,12 @@ public class Key implements Comparable<Key> {
private static final int MORE_KEYS_FLAGS_FIXED_COLUMN_ORDER = 0x80000000;
private static final int MORE_KEYS_FLAGS_HAS_LABELS = 0x40000000;
private static final int MORE_KEYS_FLAGS_NEEDS_DIVIDERS = 0x20000000;
- private static final int MORE_KEYS_FLAGS_EMBEDDED_MORE_KEY = 0x10000000;
+ private static final int MORE_KEYS_FLAGS_NO_PANEL_AUTO_MORE_KEY = 0x10000000;
private static final String MORE_KEYS_AUTO_COLUMN_ORDER = "!autoColumnOrder!";
private static final String MORE_KEYS_FIXED_COLUMN_ORDER = "!fixedColumnOrder!";
private static final String MORE_KEYS_HAS_LABELS = "!hasLabels!";
private static final String MORE_KEYS_NEEDS_DIVIDERS = "!needsDividers!";
- private static final String MORE_KEYS_EMBEDDED_MORE_KEY = "!embeddedMoreKey!";
+ private static final String MORE_KEYS_NO_PANEL_AUTO_MORE_KEY = "!noPanelAutoMoreKey!";
/** Background type that represents different key background visual than normal one. */
public final int mBackgroundType;
@@ -281,8 +281,8 @@ public class Key implements Comparable<Key> {
if (KeySpecParser.getBooleanValue(moreKeys, MORE_KEYS_NEEDS_DIVIDERS)) {
moreKeysColumn |= MORE_KEYS_FLAGS_NEEDS_DIVIDERS;
}
- if (KeySpecParser.getBooleanValue(moreKeys, MORE_KEYS_EMBEDDED_MORE_KEY)) {
- moreKeysColumn |= MORE_KEYS_FLAGS_EMBEDDED_MORE_KEY;
+ if (KeySpecParser.getBooleanValue(moreKeys, MORE_KEYS_NO_PANEL_AUTO_MORE_KEY)) {
+ moreKeysColumn |= MORE_KEYS_FLAGS_NO_PANEL_AUTO_MORE_KEY;
}
mMoreKeysColumnAndFlags = moreKeysColumn;
@@ -453,7 +453,7 @@ public class Key implements Comparable<Key> {
} else {
label = "/" + mLabel;
}
- return String.format("%s%s %d,%d %dx%d %s/%s/%s",
+ return String.format(Locale.ROOT, "%s%s %d,%d %dx%d %s/%s/%s",
Constants.printableCode(mCode), label, mX, mY, mWidth, mHeight, mHintLabel,
KeyboardIconsSet.getIconName(mIconId), backgroundName(mBackgroundType));
}
@@ -657,8 +657,8 @@ public class Key implements Comparable<Key> {
return (mMoreKeysColumnAndFlags & MORE_KEYS_FLAGS_NEEDS_DIVIDERS) != 0;
}
- public final boolean hasEmbeddedMoreKey() {
- return (mMoreKeysColumnAndFlags & MORE_KEYS_FLAGS_EMBEDDED_MORE_KEY) != 0;
+ public final boolean hasNoPanelAutoMoreKey() {
+ return (mMoreKeysColumnAndFlags & MORE_KEYS_FLAGS_NO_PANEL_AUTO_MORE_KEY) != 0;
}
public final String getOutputText() {
diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardId.java b/java/src/com/android/inputmethod/keyboard/KeyboardId.java
index aa27067bc..4c5dd25c4 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyboardId.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardId.java
@@ -187,7 +187,7 @@ public final class KeyboardId {
public String toString() {
final String orientation = (mOrientation == Configuration.ORIENTATION_PORTRAIT)
? "port" : "land";
- return String.format("[%s %s:%s %s:%dx%d %s %s %s%s%s%s%s%s%s%s%s]",
+ return String.format(Locale.ROOT, "[%s %s:%s %s:%dx%d %s %s %s%s%s%s%s%s%s%s%s]",
elementIdToName(mElementId),
mLocale,
mSubtype.getExtraValueOf(KEYBOARD_LAYOUT_SET),
diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java b/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java
index 1fe23a330..d4051f74b 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java
@@ -36,6 +36,7 @@ import android.util.Xml;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodSubtype;
+import com.android.inputmethod.annotations.UsedForTesting;
import com.android.inputmethod.compat.EditorInfoCompatUtils;
import com.android.inputmethod.keyboard.internal.KeyboardBuilder;
import com.android.inputmethod.keyboard.internal.KeyboardParams;
@@ -424,6 +425,7 @@ public final class KeyboardLayoutSet {
SPELLCHECKER_DUMMY_KEYBOARD_HEIGHT, false);
}
+ @UsedForTesting
public static KeyboardLayoutSet createKeyboardSetForTest(final Context context,
final InputMethodSubtype subtype, final int orientation,
final boolean testCasesHaveTouchCoordinates) {
diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java b/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java
index ad08d6477..4323f7171 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java
@@ -68,8 +68,6 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions {
new KeyboardTheme(5, R.style.KeyboardTheme_IceCreamSandwich),
};
- private final AudioAndHapticFeedbackManager mFeedbackManager =
- AudioAndHapticFeedbackManager.getInstance();
private SubtypeSwitcher mSubtypeSwitcher;
private SharedPreferences mPrefs;
@@ -151,7 +149,6 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions {
mKeyboardLayoutSet = builder.build();
try {
mState.onLoadKeyboard();
- mFeedbackManager.onSettingsChanged(settingsValues);
} catch (KeyboardLayoutSetException e) {
Log.w(TAG, "loading keyboard failed: " + e.mKeyboardId, e.getCause());
LatinImeLogger.logOnException(e.mKeyboardId.toString(), e.getCause());
@@ -159,10 +156,6 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions {
}
}
- public void onRingerModeChanged() {
- mFeedbackManager.onRingerModeChanged();
- }
-
public void saveKeyboardState() {
if (getKeyboard() != null) {
mState.onSaveKeyboardState();
@@ -217,9 +210,7 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions {
}
public void onPressKey(final int code, final boolean isSinglePointer) {
- if (isVibrateAndSoundFeedbackRequired()) {
- mFeedbackManager.hapticAndAudioFeedback(code, mKeyboardView);
- }
+ hapticAndAudioFeedback(code);
mState.onPressKey(code, isSinglePointer, mLatinIME.getCurrentAutoCapsState());
}
@@ -282,68 +273,37 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions {
// Implements {@link KeyboardState.SwitchActions}.
@Override
- public void startDoubleTapTimer() {
+ public void startDoubleTapShiftKeyTimer() {
final MainKeyboardView keyboardView = getMainKeyboardView();
if (keyboardView != null) {
final TimerProxy timer = keyboardView.getTimerProxy();
- timer.startDoubleTapTimer();
+ timer.startDoubleTapShiftKeyTimer();
}
}
// Implements {@link KeyboardState.SwitchActions}.
@Override
- public void cancelDoubleTapTimer() {
+ public void cancelDoubleTapShiftKeyTimer() {
final MainKeyboardView keyboardView = getMainKeyboardView();
if (keyboardView != null) {
final TimerProxy timer = keyboardView.getTimerProxy();
- timer.cancelDoubleTapTimer();
+ timer.cancelDoubleTapShiftKeyTimer();
}
}
// Implements {@link KeyboardState.SwitchActions}.
@Override
- public boolean isInDoubleTapTimeout() {
+ public boolean isInDoubleTapShiftKeyTimeout() {
final MainKeyboardView keyboardView = getMainKeyboardView();
return (keyboardView != null)
- ? keyboardView.getTimerProxy().isInDoubleTapTimeout() : false;
- }
-
- // Implements {@link KeyboardState.SwitchActions}.
- @Override
- public void startLongPressTimer(final int code) {
- final MainKeyboardView keyboardView = getMainKeyboardView();
- if (keyboardView != null) {
- final TimerProxy timer = keyboardView.getTimerProxy();
- timer.startLongPressTimer(code);
- }
+ ? keyboardView.getTimerProxy().isInDoubleTapShiftKeyTimeout() : false;
}
- // Implements {@link KeyboardState.SwitchActions}.
- @Override
- public void cancelLongPressTimer() {
- final MainKeyboardView keyboardView = getMainKeyboardView();
- if (keyboardView != null) {
- final TimerProxy timer = keyboardView.getTimerProxy();
- timer.cancelLongPressTimer();
+ private void hapticAndAudioFeedback(final int code) {
+ if (mKeyboardView == null || mKeyboardView.isInSlidingKeyInput()) {
+ return;
}
- }
-
- // Implements {@link KeyboardState.SwitchActions}.
- @Override
- public void hapticAndAudioFeedback(final int code) {
- mFeedbackManager.hapticAndAudioFeedback(code, mKeyboardView);
- }
-
- public void onLongPressTimeout(final int code) {
- mState.onLongPressTimeout(code);
- }
-
- public boolean isInMomentarySwitchState() {
- return mState.isInMomentarySwitchState();
- }
-
- private boolean isVibrateAndSoundFeedbackRequired() {
- return mKeyboardView != null && !mKeyboardView.isInSlidingKeyInput();
+ AudioAndHapticFeedbackManager.getInstance().hapticAndAudioFeedback(code, mKeyboardView);
}
/**
diff --git a/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java b/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java
index 6c6fc6157..7f335027f 100644
--- a/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java
+++ b/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java
@@ -57,6 +57,7 @@ import com.android.inputmethod.keyboard.internal.KeyPreviewDrawParams;
import com.android.inputmethod.keyboard.internal.PreviewPlacerView;
import com.android.inputmethod.keyboard.internal.SlidingKeyInputPreview;
import com.android.inputmethod.keyboard.internal.TouchScreenRegulator;
+import com.android.inputmethod.latin.AudioAndHapticFeedbackManager;
import com.android.inputmethod.latin.CollectionUtils;
import com.android.inputmethod.latin.Constants;
import com.android.inputmethod.latin.CoordinateUtils;
@@ -201,7 +202,7 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack
private static final int MSG_TYPING_STATE_EXPIRED = 0;
private static final int MSG_REPEAT_KEY = 1;
private static final int MSG_LONGPRESS_KEY = 2;
- private static final int MSG_DOUBLE_TAP = 3;
+ private static final int MSG_DOUBLE_TAP_SHIFT_KEY = 3;
private static final int MSG_UPDATE_BATCH_INPUT = 4;
private final int mKeyRepeatStartTimeout;
@@ -240,16 +241,14 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack
case MSG_REPEAT_KEY:
final Key currentKey = tracker.getKey();
if (currentKey != null && currentKey.mCode == msg.arg1) {
- tracker.onRegisterKey(currentKey);
+ tracker.onRepeatKey(currentKey);
+ AudioAndHapticFeedbackManager.getInstance().hapticAndAudioFeedback(
+ currentKey.mCode, keyboardView);
startKeyRepeatTimer(tracker, mKeyRepeatInterval);
}
break;
case MSG_LONGPRESS_KEY:
- if (tracker != null) {
- keyboardView.onLongPress(tracker);
- } else {
- KeyboardSwitcher.getInstance().onLongPressTimeout(msg.arg1);
- }
+ keyboardView.onLongPress(tracker);
break;
case MSG_UPDATE_BATCH_INPUT:
tracker.updateBatchInputByTimer(SystemClock.uptimeMillis());
@@ -281,23 +280,6 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack
}
@Override
- public void startLongPressTimer(final int code) {
- cancelLongPressTimer();
- final int delay;
- switch (code) {
- case Constants.CODE_SHIFT:
- delay = mLongPressShiftLockTimeout;
- break;
- default:
- delay = 0;
- break;
- }
- if (delay > 0) {
- sendMessageDelayed(obtainMessage(MSG_LONGPRESS_KEY, code, 0), delay);
- }
- }
-
- @Override
public void startLongPressTimer(final PointerTracker tracker) {
cancelLongPressTimer();
if (tracker == null) {
@@ -312,9 +294,8 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack
default:
final int longpressTimeout =
Settings.getInstance().getCurrent().mKeyLongpressTimeout;
- if (KeyboardSwitcher.getInstance().isInMomentarySwitchState()) {
- // We use longer timeout for sliding finger input started from the symbols
- // mode key.
+ if (tracker.isInSlidingKeyInputFromModifier()) {
+ // We use longer timeout for sliding finger input started from the modifier key.
delay = longpressTimeout * 3;
} else {
delay = longpressTimeout;
@@ -390,19 +371,19 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack
}
@Override
- public void startDoubleTapTimer() {
- sendMessageDelayed(obtainMessage(MSG_DOUBLE_TAP),
+ public void startDoubleTapShiftKeyTimer() {
+ sendMessageDelayed(obtainMessage(MSG_DOUBLE_TAP_SHIFT_KEY),
ViewConfiguration.getDoubleTapTimeout());
}
@Override
- public void cancelDoubleTapTimer() {
- removeMessages(MSG_DOUBLE_TAP);
+ public void cancelDoubleTapShiftKeyTimer() {
+ removeMessages(MSG_DOUBLE_TAP_SHIFT_KEY);
}
@Override
- public boolean isInDoubleTapTimeout() {
- return hasMessages(MSG_DOUBLE_TAP);
+ public boolean isInDoubleTapShiftKeyTimeout() {
+ return hasMessages(MSG_DOUBLE_TAP_SHIFT_KEY);
}
@Override
@@ -827,10 +808,6 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack
final KeyDrawParams drawParams = mKeyDrawParams;
previewText.setTextColor(drawParams.mPreviewTextColor);
final Drawable background = previewText.getBackground();
- if (background != null) {
- background.setState(KEY_PREVIEW_BACKGROUND_DEFAULT_STATE);
- background.setAlpha(PREVIEW_ALPHA);
- }
final String label = key.getPreviewLabel();
// What we show as preview should match what we show on a key top in onDraw().
if (label != null) {
@@ -884,6 +861,7 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack
if (background != null) {
final int hasMoreKeys = (key.mMoreKeys != null) ? STATE_HAS_MOREKEYS : STATE_NORMAL;
background.setState(KEY_PREVIEW_BACKGROUND_STATE_TABLE[statePosition][hasMoreKeys]);
+ background.setAlpha(PREVIEW_ALPHA);
}
ViewLayoutUtils.placeViewAt(
previewText, previewX, previewY, previewWidth, previewHeight);
@@ -987,38 +965,36 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack
/**
* Called when a key is long pressed.
* @param tracker the pointer tracker which pressed the parent key
- * @return true if the long press is handled, false otherwise. Subclasses should call the
- * method on the base class if the subclass doesn't wish to handle the call.
*/
- private boolean onLongPress(final PointerTracker tracker) {
+ private void onLongPress(final PointerTracker tracker) {
if (isShowingMoreKeysPanel()) {
- return false;
+ return;
}
final Key key = tracker.getKey();
if (key == null) {
- return false;
+ return;
}
if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
ResearchLogger.mainKeyboardView_onLongPress();
}
final int code = key.mCode;
- if (key.hasEmbeddedMoreKey()) {
+ if (key.hasNoPanelAutoMoreKey()) {
final int embeddedCode = key.mMoreKeys[0].mCode;
tracker.onLongPressed();
invokeCodeInput(embeddedCode);
invokeReleaseKey(code);
- KeyboardSwitcher.getInstance().hapticAndAudioFeedback(code);
- return true;
+ AudioAndHapticFeedbackManager.getInstance().hapticAndAudioFeedback(code, this);
+ return;
}
if (code == Constants.CODE_SPACE || code == Constants.CODE_LANGUAGE_SWITCH) {
// Long pressing the space key invokes IME switcher dialog.
if (invokeCustomRequest(LatinIME.CODE_SHOW_INPUT_METHOD_PICKER)) {
tracker.onLongPressed();
invokeReleaseKey(code);
- return true;
+ return;
}
}
- return openMoreKeysPanel(key, tracker);
+ openMoreKeysPanel(key, tracker);
}
private boolean invokeCustomRequest(final int requestCode) {
@@ -1034,10 +1010,10 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack
mKeyboardActionListener.onReleaseKey(code, false);
}
- private boolean openMoreKeysPanel(final Key key, final PointerTracker tracker) {
+ private void openMoreKeysPanel(final Key key, final PointerTracker tracker) {
final MoreKeysPanel moreKeysPanel = onCreateMoreKeysPanel(key, getContext());
if (moreKeysPanel == null) {
- return false;
+ return;
}
final int[] lastCoords = CoordinateUtils.newInstance();
@@ -1059,7 +1035,6 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack
final int translatedX = moreKeysPanel.translateX(CoordinateUtils.x(lastCoords));
final int translatedY = moreKeysPanel.translateY(CoordinateUtils.y(lastCoords));
tracker.onShowMoreKeysPanel(translatedX, translatedY, moreKeysPanel);
- return true;
}
public boolean isInSlidingKeyInput() {
diff --git a/java/src/com/android/inputmethod/keyboard/PointerTracker.java b/java/src/com/android/inputmethod/keyboard/PointerTracker.java
index 174239325..958aaf569 100644
--- a/java/src/com/android/inputmethod/keyboard/PointerTracker.java
+++ b/java/src/com/android/inputmethod/keyboard/PointerTracker.java
@@ -92,11 +92,10 @@ public final class PointerTracker implements PointerTrackerQueue.Element {
public boolean isTypingState();
public void startKeyRepeatTimer(PointerTracker tracker);
public void startLongPressTimer(PointerTracker tracker);
- public void startLongPressTimer(int code);
public void cancelLongPressTimer();
- public void startDoubleTapTimer();
- public void cancelDoubleTapTimer();
- public boolean isInDoubleTapTimeout();
+ public void startDoubleTapShiftKeyTimer();
+ public void cancelDoubleTapShiftKeyTimer();
+ public boolean isInDoubleTapShiftKeyTimeout();
public void cancelKeyTimers();
public void startUpdateBatchInputTimer(PointerTracker tracker);
public void cancelUpdateBatchInputTimer(PointerTracker tracker);
@@ -112,15 +111,13 @@ public final class PointerTracker implements PointerTrackerQueue.Element {
@Override
public void startLongPressTimer(PointerTracker tracker) {}
@Override
- public void startLongPressTimer(int code) {}
- @Override
public void cancelLongPressTimer() {}
@Override
- public void startDoubleTapTimer() {}
+ public void startDoubleTapShiftKeyTimer() {}
@Override
- public void cancelDoubleTapTimer() {}
+ public void cancelDoubleTapShiftKeyTimer() {}
@Override
- public boolean isInDoubleTapTimeout() { return false; }
+ public boolean isInDoubleTapShiftKeyTimeout() { return false; }
@Override
public void cancelKeyTimers() {}
@Override
@@ -1266,15 +1263,13 @@ public final class PointerTracker implements PointerTrackerQueue.Element {
if (!key.isRepeatable()) return;
// Don't start key repeat when we are in sliding input mode.
if (mIsInSlidingKeyInput) return;
- onRegisterKey(key);
+ onRepeatKey(key);
mTimerProxy.startKeyRepeatTimer(this);
}
- public void onRegisterKey(final Key key) {
- if (key != null) {
- detectAndSendKey(key, key.mX, key.mY, SystemClock.uptimeMillis());
- mTimerProxy.startTypingStateTimer(key);
- }
+ public void onRepeatKey(final Key key) {
+ detectAndSendKey(key, key.mX, key.mY, SystemClock.uptimeMillis());
+ mTimerProxy.startTypingStateTimer(key);
}
private boolean isMajorEnoughMoveToBeOnNewKey(final int x, final int y, final long eventTime,
diff --git a/java/src/com/android/inputmethod/keyboard/internal/GestureStrokeWithPreviewPoints.java b/java/src/com/android/inputmethod/keyboard/internal/GestureStrokeWithPreviewPoints.java
index b31f00b62..8deadbf96 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/GestureStrokeWithPreviewPoints.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/GestureStrokeWithPreviewPoints.java
@@ -58,7 +58,7 @@ public final class GestureStrokeWithPreviewPoints extends GestureStroke {
}
private static double degreeToRadian(final int degree) {
- return (double)degree / 180.0d * Math.PI;
+ return degree / 180.0d * Math.PI;
}
public GestureStrokePreviewParams(final TypedArray mainKeyboardViewAttr) {
@@ -125,8 +125,18 @@ public final class GestureStrokeWithPreviewPoints extends GestureStroke {
}
+ /**
+ * Append sampled preview points.
+ *
+ * @param eventTimes the event time array of gesture trail to be drawn.
+ * @param xCoords the x-coordinates array of gesture trail to be drawn.
+ * @param yCoords the y-coordinates array of gesture trail to be drawn.
+ * @param types the point types array of gesture trail. This is valid only when
+ * {@link GestureTrail#DEBUG_SHOW_POINTS} is true.
+ */
public void appendPreviewStroke(final ResizableIntArray eventTimes,
- final ResizableIntArray xCoords, final ResizableIntArray yCoords) {
+ final ResizableIntArray xCoords, final ResizableIntArray yCoords,
+ final ResizableIntArray types) {
final int length = mPreviewEventTimes.getLength() - mLastPreviewSize;
if (length <= 0) {
return;
@@ -134,6 +144,9 @@ public final class GestureStrokeWithPreviewPoints extends GestureStroke {
eventTimes.append(mPreviewEventTimes, mLastPreviewSize, length);
xCoords.append(mPreviewXCoordinates, mLastPreviewSize, length);
yCoords.append(mPreviewYCoordinates, mLastPreviewSize, length);
+ if (GestureTrail.DEBUG_SHOW_POINTS) {
+ types.fill(GestureTrail.POINT_TYPE_SAMPLED, types.getLength(), length);
+ }
mLastPreviewSize = mPreviewEventTimes.getLength();
}
@@ -148,6 +161,8 @@ public final class GestureStrokeWithPreviewPoints extends GestureStroke {
* @param eventTimes the event time array of gesture trail to be drawn.
* @param xCoords the x-coordinates array of gesture trail to be drawn.
* @param yCoords the y-coordinates array of gesture trail to be drawn.
+ * @param types the point types array of gesture trail. This is valid only when
+ * {@link GestureTrail#DEBUG_SHOW_POINTS} is true.
* @return the start index of the last interpolated segment of input arrays.
*/
public int interpolateStrokeAndReturnStartIndexOfLastSegment(final int lastInterpolatedIndex,
@@ -189,7 +204,7 @@ public final class GestureStrokeWithPreviewPoints extends GestureStroke {
eventTimes.add(d1, (int)(dt * t) + t1);
xCoords.add(d1, (int)mInterpolator.mInterpolatedX);
yCoords.add(d1, (int)mInterpolator.mInterpolatedY);
- if (GestureTrail.DBG_SHOW_POINTS) {
+ if (GestureTrail.DEBUG_SHOW_POINTS) {
types.add(d1, GestureTrail.POINT_TYPE_INTERPOLATED);
}
d1++;
@@ -197,7 +212,7 @@ public final class GestureStrokeWithPreviewPoints extends GestureStroke {
eventTimes.add(d1, pt[p2]);
xCoords.add(d1, px[p2]);
yCoords.add(d1, py[p2]);
- if (GestureTrail.DBG_SHOW_POINTS) {
+ if (GestureTrail.DEBUG_SHOW_POINTS) {
types.add(d1, GestureTrail.POINT_TYPE_SAMPLED);
}
}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/GestureTrail.java b/java/src/com/android/inputmethod/keyboard/internal/GestureTrail.java
index 03dd1c372..0f3cd7887 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/GestureTrail.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/GestureTrail.java
@@ -36,10 +36,11 @@ import com.android.inputmethod.latin.ResizableIntArray;
* @attr ref R.styleable#MainKeyboardView_gestureTrailWidth
*/
final class GestureTrail {
- public static final boolean DBG_SHOW_POINTS = false;
- public static final int POINT_TYPE_SAMPLED = 0;
- public static final int POINT_TYPE_INTERPOLATED = 1;
- public static final int POINT_TYPE_COMPROMISED = 2;
+ public static final boolean DEBUG_SHOW_POINTS = false;
+ public static final int POINT_TYPE_SAMPLED = 1;
+ public static final int POINT_TYPE_INTERPOLATED = 2;
+ private static final int FADEOUT_START_DELAY_FOR_DEBUG = 2000; // millisecond
+ private static final int FADEOUT_DURATION_FOR_DEBUG = 200; // millisecond
private static final int DEFAULT_CAPACITY = GestureStrokeWithPreviewPoints.PREVIEW_CAPACITY;
@@ -48,7 +49,7 @@ final class GestureTrail {
private final ResizableIntArray mYCoordinates = new ResizableIntArray(DEFAULT_CAPACITY);
private final ResizableIntArray mEventTimes = new ResizableIntArray(DEFAULT_CAPACITY);
private final ResizableIntArray mPointTypes = new ResizableIntArray(
- DBG_SHOW_POINTS ? DEFAULT_CAPACITY : 0);
+ DEBUG_SHOW_POINTS ? DEFAULT_CAPACITY : 0);
private int mCurrentStrokeId = -1;
// The wall time of the zero value in {@link #mEventTimes}
private long mCurrentTimeBase;
@@ -83,10 +84,12 @@ final class GestureTrail {
R.styleable.MainKeyboardView_gestureTrailShadowRatio, 0);
mTrailShadowEnabled = (trailShadowRatioInt > 0);
mTrailShadowRatio = (float)trailShadowRatioInt / (float)PERCENTAGE_INT;
- mFadeoutStartDelay = DBG_SHOW_POINTS ? 2000 : mainKeyboardViewAttr.getInt(
- R.styleable.MainKeyboardView_gestureTrailFadeoutStartDelay, 0);
- mFadeoutDuration = DBG_SHOW_POINTS ? 200 : mainKeyboardViewAttr.getInt(
- R.styleable.MainKeyboardView_gestureTrailFadeoutDuration, 0);
+ mFadeoutStartDelay = DEBUG_SHOW_POINTS ? FADEOUT_START_DELAY_FOR_DEBUG
+ : mainKeyboardViewAttr.getInt(
+ R.styleable.MainKeyboardView_gestureTrailFadeoutStartDelay, 0);
+ mFadeoutDuration = DEBUG_SHOW_POINTS ? FADEOUT_DURATION_FOR_DEBUG
+ : mainKeyboardViewAttr.getInt(
+ R.styleable.MainKeyboardView_gestureTrailFadeoutDuration, 0);
mTrailLingerDuration = mFadeoutStartDelay + mFadeoutDuration;
mUpdateInterval = mainKeyboardViewAttr.getInt(
R.styleable.MainKeyboardView_gestureTrailUpdateInterval, 0);
@@ -117,7 +120,7 @@ final class GestureTrail {
private void addStrokeLocked(final GestureStrokeWithPreviewPoints stroke, final long downTime) {
final int trailSize = mEventTimes.getLength();
- stroke.appendPreviewStroke(mEventTimes, mXCoordinates, mYCoordinates);
+ stroke.appendPreviewStroke(mEventTimes, mXCoordinates, mYCoordinates, mPointTypes);
if (mEventTimes.getLength() == trailSize) {
return;
}
@@ -255,23 +258,15 @@ final class GestureTrail {
final int alpha = getAlpha(elapsedTime, params);
paint.setAlpha(alpha);
canvas.drawPath(path, paint);
- if (DBG_SHOW_POINTS) {
- if (pointTypes[i] == POINT_TYPE_INTERPOLATED) {
- paint.setColor(Color.RED);
- } else if (pointTypes[i] == POINT_TYPE_SAMPLED) {
- paint.setColor(0xFFA000FF);
- } else {
- paint.setColor(Color.GREEN);
- }
- canvas.drawCircle(p1x - 1, p1y - 1, 2, paint);
- paint.setColor(params.mTrailColor);
- }
}
}
p1x = p2x;
p1y = p2y;
r1 = r2;
}
+ if (DEBUG_SHOW_POINTS) {
+ debugDrawPoints(canvas, startIndex, trailSize, paint);
+ }
}
final int newSize = trailSize - startIndex;
@@ -281,11 +276,14 @@ final class GestureTrail {
System.arraycopy(eventTimes, startIndex, eventTimes, 0, newSize);
System.arraycopy(xCoords, startIndex, xCoords, 0, newSize);
System.arraycopy(yCoords, startIndex, yCoords, 0, newSize);
+ if (DEBUG_SHOW_POINTS) {
+ System.arraycopy(pointTypes, startIndex, pointTypes, 0, newSize);
+ }
}
mEventTimes.setLength(newSize);
mXCoordinates.setLength(newSize);
mYCoordinates.setLength(newSize);
- if (DBG_SHOW_POINTS) {
+ if (DEBUG_SHOW_POINTS) {
mPointTypes.setLength(newSize);
}
// The start index of the last segment of the stroke
@@ -295,4 +293,26 @@ final class GestureTrail {
}
return newSize > 0;
}
+
+ private void debugDrawPoints(final Canvas canvas, final int startIndex, final int endIndex,
+ final Paint paint) {
+ final int[] xCoords = mXCoordinates.getPrimitiveArray();
+ final int[] yCoords = mYCoordinates.getPrimitiveArray();
+ final int[] pointTypes = mPointTypes.getPrimitiveArray();
+ // {@link Paint} that is zero width stroke and anti alias off draws exactly 1 pixel.
+ paint.setAntiAlias(false);
+ paint.setStrokeWidth(0);
+ for (int i = startIndex; i < endIndex; i++) {
+ final int pointType = pointTypes[i];
+ if (pointType == POINT_TYPE_INTERPOLATED) {
+ paint.setColor(Color.RED);
+ } else if (pointType == POINT_TYPE_SAMPLED) {
+ paint.setColor(0xFFA000FF);
+ } else {
+ paint.setColor(Color.GREEN);
+ }
+ canvas.drawPoint(getXCoordValue(xCoords[i]), yCoords[i], paint);
+ }
+ paint.setAntiAlias(true);
+ }
}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/HermiteInterpolator.java b/java/src/com/android/inputmethod/keyboard/internal/HermiteInterpolator.java
index 0ec8153f5..b526a942a 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/HermiteInterpolator.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/HermiteInterpolator.java
@@ -16,8 +16,6 @@
package com.android.inputmethod.keyboard.internal;
-import com.android.inputmethod.annotations.UsedForTesting;
-
/**
* Interpolates XY-coordinates using Cubic Hermite Curve.
*/
@@ -54,7 +52,6 @@ public final class HermiteInterpolator {
* @param minPos the minimum index of left-open interval of valid data.
* @param maxPos the maximum index of left-open interval of valid data.
*/
- @UsedForTesting
public void reset(final int[] xCoords, final int[] yCoords, final int minPos,
final int maxPos) {
mXCoords = xCoords;
@@ -79,7 +76,6 @@ public final class HermiteInterpolator {
* valid points, <code>p3</code> must be equal or greater than <code>maxPos</code> of
* {@link #reset(int[],int[],int,int)}.
*/
- @UsedForTesting
public void setInterval(final int p0, final int p1, final int p2, final int p3) {
mP1X = mXCoords[p1];
mP1Y = mYCoords[p1];
@@ -152,7 +148,6 @@ public final class HermiteInterpolator {
*
* @param t the interpolation parameter. The value must be in close interval <code>[0,1]</code>.
*/
- @UsedForTesting
public void interpolate(final float t) {
final float omt = 1.0f - t;
final float tm2 = 2.0f * t;
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java b/java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java
index b1813a141..ba449eeb3 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java
@@ -53,7 +53,9 @@ public final class KeySpecParser {
private static final int MAX_STRING_REFERENCE_INDIRECTION = 10;
// Constants for parsing.
- private static final char LABEL_END = '|';
+ private static final char COMMA = ',';
+ private static final char BACKSLASH = '\\';
+ private static final char VERTICAL_BAR = '|';
private static final String PREFIX_TEXT = "!text/";
static final String PREFIX_ICON = "!icon/";
private static final String PREFIX_CODE = "!code/";
@@ -64,6 +66,59 @@ public final class KeySpecParser {
// Intentional empty constructor for utility class.
}
+ /**
+ * Split the text containing multiple key specifications separated by commas into an array of
+ * key specifications.
+ * A key specification can contain a character escaped by the backslash character, including a
+ * comma character.
+ * Note that an empty key specification will be eliminated from the result array.
+ *
+ * @param text the text containing multiple key specifications.
+ * @return an array of key specification text. Null if the specified <code>text</code> is empty
+ * or has no key specifications.
+ */
+ public static String[] splitKeySpecs(final String text) {
+ final int size = text.length();
+ if (size == 0) {
+ return null;
+ }
+ // Optimization for one-letter key specification.
+ if (size == 1) {
+ return text.charAt(0) == COMMA ? null : new String[] { text };
+ }
+
+ ArrayList<String> list = null;
+ int start = 0;
+ // The characters in question in this loop are COMMA and BACKSLASH. These characters never
+ // match any high or low surrogate character. So it is OK to iterate through with char
+ // index.
+ for (int pos = 0; pos < size; pos++) {
+ final char c = text.charAt(pos);
+ if (c == COMMA) {
+ // Skip empty entry.
+ if (pos - start > 0) {
+ if (list == null) {
+ list = CollectionUtils.newArrayList();
+ }
+ list.add(text.substring(start, pos));
+ }
+ // Skip comma
+ start = pos + 1;
+ } else if (c == BACKSLASH) {
+ // Skip escape character and escaped character.
+ pos++;
+ }
+ }
+ final String remain = (size - start > 0) ? text.substring(start) : null;
+ if (list == null) {
+ return remain != null ? new String[] { remain } : null;
+ }
+ if (remain != null) {
+ list.add(remain);
+ }
+ return list.toArray(new String[list.size()]);
+ }
+
private static boolean hasIcon(final String moreKeySpec) {
return moreKeySpec.startsWith(PREFIX_ICON);
}
@@ -78,14 +133,14 @@ public final class KeySpecParser {
}
private static String parseEscape(final String text) {
- if (text.indexOf(Constants.CSV_ESCAPE) < 0) {
+ if (text.indexOf(BACKSLASH) < 0) {
return text;
}
final int length = text.length();
final StringBuilder sb = new StringBuilder();
for (int pos = 0; pos < length; pos++) {
final char c = text.charAt(pos);
- if (c == Constants.CSV_ESCAPE && pos + 1 < length) {
+ if (c == BACKSLASH && pos + 1 < length) {
// Skip escape char
pos++;
sb.append(text.charAt(pos));
@@ -97,20 +152,20 @@ public final class KeySpecParser {
}
private static int indexOfLabelEnd(final String moreKeySpec, final int start) {
- if (moreKeySpec.indexOf(Constants.CSV_ESCAPE, start) < 0) {
- final int end = moreKeySpec.indexOf(LABEL_END, start);
+ if (moreKeySpec.indexOf(BACKSLASH, start) < 0) {
+ final int end = moreKeySpec.indexOf(VERTICAL_BAR, start);
if (end == 0) {
- throw new KeySpecParserError(LABEL_END + " at " + start + ": " + moreKeySpec);
+ throw new KeySpecParserError(VERTICAL_BAR + " at " + start + ": " + moreKeySpec);
}
return end;
}
final int length = moreKeySpec.length();
for (int pos = start; pos < length; pos++) {
final char c = moreKeySpec.charAt(pos);
- if (c == Constants.CSV_ESCAPE && pos + 1 < length) {
+ if (c == BACKSLASH && pos + 1 < length) {
// Skip escape char
pos++;
- } else if (c == LABEL_END) {
+ } else if (c == VERTICAL_BAR) {
return pos;
}
}
@@ -136,9 +191,9 @@ public final class KeySpecParser {
return null;
}
if (indexOfLabelEnd(moreKeySpec, end + 1) >= 0) {
- throw new KeySpecParserError("Multiple " + LABEL_END + ": " + moreKeySpec);
+ throw new KeySpecParserError("Multiple " + VERTICAL_BAR + ": " + moreKeySpec);
}
- return parseEscape(moreKeySpec.substring(end + /* LABEL_END */1));
+ return parseEscape(moreKeySpec.substring(end + /* VERTICAL_BAR */1));
}
static String getOutputText(final String moreKeySpec) {
@@ -169,7 +224,7 @@ public final class KeySpecParser {
if (hasCode(moreKeySpec)) {
final int end = indexOfLabelEnd(moreKeySpec, 0);
if (indexOfLabelEnd(moreKeySpec, end + 1) >= 0) {
- throw new KeySpecParserError("Multiple " + LABEL_END + ": " + moreKeySpec);
+ throw new KeySpecParserError("Multiple " + VERTICAL_BAR + ": " + moreKeySpec);
}
return parseCode(moreKeySpec.substring(end + 1), codesSet, CODE_UNSPECIFIED);
}
@@ -204,7 +259,7 @@ public final class KeySpecParser {
public static int getIconId(final String moreKeySpec) {
if (moreKeySpec != null && hasIcon(moreKeySpec)) {
- final int end = moreKeySpec.indexOf(LABEL_END, PREFIX_ICON.length());
+ final int end = moreKeySpec.indexOf(VERTICAL_BAR, PREFIX_ICON.length());
final String name = (end < 0) ? moreKeySpec.substring(PREFIX_ICON.length())
: moreKeySpec.substring(PREFIX_ICON.length(), end);
return KeyboardIconsSet.getIconId(name);
@@ -351,7 +406,7 @@ public final class KeySpecParser {
final String name = text.substring(pos + prefixLen, end);
sb.append(textsSet.getText(name));
pos = end - 1;
- } else if (c == Constants.CSV_ESCAPE) {
+ } else if (c == BACKSLASH) {
if (sb != null) {
// Append both escape character and escaped character.
sb.append(text.substring(pos, Math.min(pos + 2, size)));
@@ -366,7 +421,6 @@ public final class KeySpecParser {
text = sb.toString();
}
} while (sb != null);
-
return text;
}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyStyle.java b/java/src/com/android/inputmethod/keyboard/internal/KeyStyle.java
index 5db3ebbd1..f65056948 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyStyle.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyStyle.java
@@ -18,8 +18,6 @@ package com.android.inputmethod.keyboard.internal;
import android.content.res.TypedArray;
-import com.android.inputmethod.latin.StringUtils;
-
public abstract class KeyStyle {
private final KeyboardTextsSet mTextsSet;
@@ -42,7 +40,7 @@ public abstract class KeyStyle {
protected String[] parseStringArray(final TypedArray a, final int index) {
if (a.hasValue(index)) {
final String text = KeySpecParser.resolveTextReference(a.getString(index), mTextsSet);
- return StringUtils.parseCsvString(text);
+ return KeySpecParser.splitKeySpecs(text);
}
return null;
}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardCodesSet.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardCodesSet.java
index 3e25c3b86..a9e04bccf 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardCodesSet.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardCodesSet.java
@@ -43,6 +43,7 @@ public final class KeyboardCodesSet {
"key_enter",
"key_space",
"key_shift",
+ "key_capslock",
"key_switch_alpha_symbol",
"key_output_text",
"key_delete",
@@ -79,6 +80,7 @@ public final class KeyboardCodesSet {
Constants.CODE_ENTER,
Constants.CODE_SPACE,
Constants.CODE_SHIFT,
+ Constants.CODE_CAPSLOCK,
Constants.CODE_SWITCH_ALPHA_SYMBOL,
Constants.CODE_OUTPUT_TEXT,
Constants.CODE_DELETE,
@@ -116,6 +118,7 @@ public final class KeyboardCodesSet {
DEFAULT[12],
DEFAULT[13],
DEFAULT[14],
+ DEFAULT[15],
CODE_RIGHT_PARENTHESIS,
CODE_LEFT_PARENTHESIS,
CODE_GREATER_THAN_SIGN,
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardState.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardState.java
index 6af1bd75f..e1cee427e 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardState.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardState.java
@@ -30,7 +30,7 @@ import com.android.inputmethod.latin.RecapitalizeStatus;
* The input events are {@link #onLoadKeyboard()}, {@link #onSaveKeyboardState()},
* {@link #onPressKey(int,boolean,int)}, {@link #onReleaseKey(int,boolean)},
* {@link #onCodeInput(int,int)}, {@link #onFinishSlidingInput()}, {@link #onCancelInput()},
- * {@link #onUpdateShiftState(int,int)}, {@link #onLongPressTimeout(int)}.
+ * {@link #onUpdateShiftState(int,int)}.
*
* The actions are {@link SwitchActions}'s methods.
*/
@@ -53,12 +53,9 @@ public final class KeyboardState {
*/
public void requestUpdatingShiftState();
- public void startDoubleTapTimer();
- public boolean isInDoubleTapTimeout();
- public void cancelDoubleTapTimer();
- public void startLongPressTimer(int code);
- public void cancelLongPressTimer();
- public void hapticAndAudioFeedback(int code);
+ public void startDoubleTapShiftKeyTimer();
+ public boolean isInDoubleTapShiftKeyTimeout();
+ public void cancelDoubleTapShiftKeyTimer();
}
private final SwitchActions mSwitchActions;
@@ -321,13 +318,16 @@ public final class KeyboardState {
Log.d(TAG, "onPressKey: code=" + Constants.printableCode(code)
+ " single=" + isSinglePointer + " autoCaps=" + autoCaps + " " + this);
}
+ if (code != Constants.CODE_SHIFT) {
+ // Because the double tap shift key timer is to detect two consecutive shift key press,
+ // it should be canceled when a non-shift key is pressed.
+ mSwitchActions.cancelDoubleTapShiftKeyTimer();
+ }
if (code == Constants.CODE_SHIFT) {
onPressShift();
} else if (code == Constants.CODE_SWITCH_ALPHA_SYMBOL) {
onPressSymbol();
} else {
- mSwitchActions.cancelDoubleTapTimer();
- mSwitchActions.cancelLongPressTimer();
mLongPressShiftLockFired = false;
mShiftKeyState.onOtherKeyPressed();
mSymbolKeyState.onOtherKeyPressed();
@@ -381,16 +381,6 @@ public final class KeyboardState {
mSymbolKeyState.onRelease();
}
- public void onLongPressTimeout(final int code) {
- if (DEBUG_EVENT) {
- Log.d(TAG, "onLongPressTimeout: code=" + Constants.printableCode(code) + " " + this);
- }
- if (mIsAlphabetMode && code == Constants.CODE_SHIFT) {
- mLongPressShiftLockFired = true;
- mSwitchActions.hapticAndAudioFeedback(code);
- }
- }
-
public void onUpdateShiftState(final int autoCaps, final int recapitalizeMode) {
if (DEBUG_EVENT) {
Log.d(TAG, "onUpdateShiftState: autoCaps=" + autoCaps + ", recapitalizeMode="
@@ -450,12 +440,14 @@ public final class KeyboardState {
mLongPressShiftLockFired = false;
// If we are recapitalizing, we don't do any of the normal processing, including
// importantly the double tap timer.
- if (RecapitalizeStatus.NOT_A_RECAPITALIZE_MODE != mRecapitalizeMode) return;
+ if (RecapitalizeStatus.NOT_A_RECAPITALIZE_MODE != mRecapitalizeMode) {
+ return;
+ }
if (mIsAlphabetMode) {
- mIsInDoubleTapShiftKey = mSwitchActions.isInDoubleTapTimeout();
+ mIsInDoubleTapShiftKey = mSwitchActions.isInDoubleTapShiftKeyTimeout();
if (!mIsInDoubleTapShiftKey) {
// This is first tap.
- mSwitchActions.startDoubleTapTimer();
+ mSwitchActions.startDoubleTapShiftKeyTimer();
}
if (mIsInDoubleTapShiftKey) {
if (mAlphabetShiftState.isManualShifted() || mIsInAlphabetUnshiftedFromShifted) {
@@ -486,7 +478,6 @@ public final class KeyboardState {
setShifted(MANUAL_SHIFT);
mShiftKeyState.onPress();
}
- mSwitchActions.startLongPressTimer(Constants.CODE_SHIFT);
}
} else {
// In symbol mode, just toggle symbol and symbol more keyboard.
@@ -576,11 +567,6 @@ public final class KeyboardState {
}
}
- public boolean isInMomentarySwitchState() {
- return mSwitchState == SWITCH_STATE_MOMENTARY_ALPHA_AND_SYMBOL
- || mSwitchState == SWITCH_STATE_MOMENTARY_SYMBOL_AND_MORE;
- }
-
private static boolean isSpaceCharacter(final int c) {
return c == Constants.CODE_SPACE || c == Constants.CODE_ENTER;
}
@@ -624,6 +610,12 @@ public final class KeyboardState {
break;
}
+ if (code == Constants.CODE_CAPSLOCK) {
+ // Changing shift lock state will be handled at {@link #onPressShift()} when the shift
+ // key is released.
+ mLongPressShiftLockFired = true;
+ }
+
// If the code is a letter, update keyboard shift state.
if (Constants.isLetterCode(code)) {
updateAlphabetShiftState(autoCaps, RecapitalizeStatus.NOT_A_RECAPITALIZE_MODE);
diff --git a/java/src/com/android/inputmethod/keyboard/internal/MatrixUtils.java b/java/src/com/android/inputmethod/keyboard/internal/MatrixUtils.java
new file mode 100644
index 000000000..4916a15b5
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/internal/MatrixUtils.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.inputmethod.keyboard.internal;
+
+import com.android.inputmethod.annotations.UsedForTesting;
+
+import android.util.Log;
+
+import java.util.Arrays;
+
+/**
+ * Utilities for matrix operations. Don't instantiate objects inside this class to prevent
+ * unexpected performance regressions.
+ */
+@UsedForTesting
+public class MatrixUtils {
+ private static final String TAG = MatrixUtils.class.getSimpleName();
+ public static class MatrixOperationFailedException extends Exception {
+ private static final String TAG = MatrixOperationFailedException.class.getSimpleName();
+ private static final long serialVersionUID = 4384485606788583829L;
+
+ public MatrixOperationFailedException(String msg) {
+ super(msg);
+ Log.d(TAG, msg);
+ }
+ }
+
+ /**
+ * A utility function to inverse matrix.
+ * Find a pivot and swap the row of squareMatrix0 and squareMatrix1
+ */
+ private static void findPivotAndSwapRow(final int row, final float[][] squareMatrix0,
+ final float[][] squareMatrix1, final int size) {
+ int ip = row;
+ float pivot = Math.abs(squareMatrix0[row][row]);
+ for (int i = row + 1; i < size; ++i) {
+ if (pivot < Math.abs(squareMatrix0[i][row])) {
+ ip = i;
+ pivot = Math.abs(squareMatrix0[i][row]);
+ }
+ }
+ if (ip != row) {
+ for (int j = 0; j < size; ++j) {
+ final float temp0 = squareMatrix0[ip][j];
+ squareMatrix0[ip][j] = squareMatrix0[row][j];
+ squareMatrix0[row][j] = temp0;
+ final float temp1 = squareMatrix1[ip][j];
+ squareMatrix1[ip][j] = squareMatrix1[row][j];
+ squareMatrix1[row][j] = temp1;
+ }
+ }
+ }
+
+ /**
+ * A utility function to inverse matrix. This function calculates answer for each row by
+ * sweeping method of Gauss Jordan elimination
+ */
+ private static void sweep(final int row, final float[][] squareMatrix0,
+ final float[][] squareMatrix1, final int size) throws MatrixOperationFailedException {
+ final float pivot = squareMatrix0[row][row];
+ if (pivot == 0) {
+ throw new MatrixOperationFailedException("Inverse failed. Invalid pivot");
+ }
+ for (int j = 0; j < size; ++j) {
+ squareMatrix0[row][j] /= pivot;
+ squareMatrix1[row][j] /= pivot;
+ }
+ for (int i = 0; i < size; i++) {
+ final float sweepTargetValue = squareMatrix0[i][row];
+ if (i != row) {
+ for (int j = row; j < size; ++j) {
+ squareMatrix0[i][j] -= sweepTargetValue * squareMatrix0[row][j];
+ }
+ for (int j = 0; j < size; ++j) {
+ squareMatrix1[i][j] -= sweepTargetValue * squareMatrix1[row][j];
+ }
+ }
+ }
+ }
+
+ /**
+ * A function to inverse matrix.
+ * The inverse matrix of squareMatrix will be output to inverseMatrix. Please notice that
+ * the value of squareMatrix is modified in this function and can't be resuable.
+ */
+ @UsedForTesting
+ public static void inverse(final float[][] squareMatrix,
+ final float[][] inverseMatrix) throws MatrixOperationFailedException {
+ final int size = squareMatrix.length;
+ if (squareMatrix[0].length != size || inverseMatrix.length != size
+ || inverseMatrix[0].length != size) {
+ throw new MatrixOperationFailedException(
+ "--- invalid length. column should be 2 times larger than row.");
+ }
+ for (int i = 0; i < size; ++i) {
+ Arrays.fill(inverseMatrix[i], 0.0f);
+ inverseMatrix[i][i] = 1.0f;
+ }
+ for (int i = 0; i < size; ++i) {
+ findPivotAndSwapRow(i, squareMatrix, inverseMatrix, size);
+ sweep(i, squareMatrix, inverseMatrix, size);
+ }
+ }
+
+ /**
+ * A matrix operation to multiply m0 and m1.
+ */
+ @UsedForTesting
+ public static void multiply(final float[][] m0, final float[][] m1,
+ final float[][] retval) throws MatrixOperationFailedException {
+ if (m0[0].length != m1.length) {
+ throw new MatrixOperationFailedException(
+ "--- invalid length for multiply " + m0[0].length + ", " + m1.length);
+ }
+ final int m0h = m0.length;
+ final int m0w = m0[0].length;
+ final int m1w = m1[0].length;
+ if (retval.length != m0h || retval[0].length != m1w) {
+ throw new MatrixOperationFailedException(
+ "--- invalid length of retval " + retval.length + ", " + retval[0].length);
+ }
+
+ for (int i = 0; i < m0h; i++) {
+ Arrays.fill(retval[i], 0);
+ for (int j = 0; j < m1w; j++) {
+ for (int k = 0; k < m0w; k++) {
+ retval[i][j] += m0[i][k] * m1[k][j];
+ }
+ }
+ }
+ }
+
+ /**
+ * A utility function to dump the specified matrix in a readable way
+ */
+ @UsedForTesting
+ public static void dump(final String title, final float[][] a) {
+ final int column = a[0].length;
+ final int row = a.length;
+ Log.d(TAG, "Dump matrix: " + title);
+ Log.d(TAG, "/*---------------------");
+ final StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < row; ++i) {
+ sb.setLength(0);
+ for (int j = 0; j < column; ++j) {
+ sb.append(String.format("%4f", a[i][j])).append(' ');
+ }
+ Log.d(TAG, sb.toString());
+ }
+ Log.d(TAG, "---------------------*/");
+ }
+}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/SmoothingUtils.java b/java/src/com/android/inputmethod/keyboard/internal/SmoothingUtils.java
new file mode 100644
index 000000000..e5665bcdd
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/internal/SmoothingUtils.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.inputmethod.keyboard.internal;
+
+import com.android.inputmethod.annotations.UsedForTesting;
+import com.android.inputmethod.keyboard.internal.MatrixUtils.MatrixOperationFailedException;
+
+import android.util.Log;
+
+import java.util.Arrays;
+
+/**
+ * Utilities to smooth coordinates. Currently, we calculate 3d least squares formula by using
+ * Lagrangian smoothing
+ */
+@UsedForTesting
+public class SmoothingUtils {
+ private static final String TAG = SmoothingUtils.class.getSimpleName();
+ private static final boolean DEBUG = false;
+
+ private SmoothingUtils() {
+ // not allowed to instantiate publicly
+ }
+
+ /**
+ * Find a most likely 3d least squares formula for specified coordinates.
+ * "retval" should be a 1x4 size matrix.
+ */
+ @UsedForTesting
+ public static void get3DParameters(final float[] xs, final float[] ys,
+ final float[][] retval) throws MatrixOperationFailedException {
+ final int COEFF_COUNT = 4; // Coefficient count for 3d smoothing
+ if (retval.length != COEFF_COUNT || retval[0].length != 1) {
+ Log.d(TAG, "--- invalid length of 3d retval " + retval.length + ", "
+ + retval[0].length);
+ return;
+ }
+ final int N = xs.length;
+ // TODO: Never isntantiate the matrix
+ final float[][] m0 = new float[COEFF_COUNT][COEFF_COUNT];
+ final float[][] m0Inv = new float[COEFF_COUNT][COEFF_COUNT];
+ final float[][] m1 = new float[COEFF_COUNT][N];
+ final float[][] m2 = new float[N][1];
+
+ // m0
+ for (int i = 0; i < COEFF_COUNT; ++i) {
+ Arrays.fill(m0[i], 0);
+ for (int j = 0; j < COEFF_COUNT; ++j) {
+ final int pow = i + j;
+ for (int k = 0; k < N; ++k) {
+ m0[i][j] += (float) Math.pow((double) xs[k], pow);
+ }
+ }
+ }
+ // m0Inv
+ MatrixUtils.inverse(m0, m0Inv);
+ if (DEBUG) {
+ MatrixUtils.dump("m0-1", m0Inv);
+ }
+
+ // m1
+ for (int i = 0; i < COEFF_COUNT; ++i) {
+ for (int j = 0; j < N; ++j) {
+ m1[i][j] = (i == 0) ? 1.0f : m1[i - 1][j] * xs[j];
+ }
+ }
+
+ // m2
+ for (int i = 0; i < N; ++i) {
+ m2[i][0] = ys[i];
+ }
+
+ final float[][] m0Invxm1 = new float[COEFF_COUNT][N];
+ if (DEBUG) {
+ MatrixUtils.dump("a0", m0Inv);
+ MatrixUtils.dump("a1", m1);
+ }
+ MatrixUtils.multiply(m0Inv, m1, m0Invxm1);
+ if (DEBUG) {
+ MatrixUtils.dump("a2", m0Invxm1);
+ MatrixUtils.dump("a3", m2);
+ }
+ MatrixUtils.multiply(m0Invxm1, m2, retval);
+ if (DEBUG) {
+ MatrixUtils.dump("result", retval);
+ }
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/AdditionalFeaturesSettingUtils.java b/java/src/com/android/inputmethod/latin/AdditionalFeaturesSettingUtils.java
new file mode 100644
index 000000000..0fdaea50c
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/AdditionalFeaturesSettingUtils.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.inputmethod.latin;
+
+import com.android.inputmethodcommon.InputMethodSettingsFragment;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+
+/**
+ * Utility class for managing additional features settings.
+ */
+public class AdditionalFeaturesSettingUtils {
+ public static final int ADDITIONAL_FEATURES_SETTINGS_SIZE = 0;
+
+ private AdditionalFeaturesSettingUtils() {
+ // This utility class is not publicly instantiable.
+ }
+
+ public static void addAdditionalFeaturesPreferences(
+ final Context context, final InputMethodSettingsFragment settingsFragment) {
+ // do nothing.
+ }
+
+ public static void readAdditionalFeaturesPreferencesIntoArray(
+ final SharedPreferences prefs, final int[] additionalFeaturesPreferences) {
+ // do nothing.
+ }
+
+ public static int[] getAdditionalNativeSuggestOptions() {
+ return Settings.getInstance().getCurrent().mAdditionalFeaturesSettingValues;
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/AdditionalSubtype.java b/java/src/com/android/inputmethod/latin/AdditionalSubtype.java
index 99b95ea98..85b14d849 100644
--- a/java/src/com/android/inputmethod/latin/AdditionalSubtype.java
+++ b/java/src/com/android/inputmethod/latin/AdditionalSubtype.java
@@ -49,13 +49,14 @@ public final class AdditionalSubtype {
&& SubtypeLocale.isExceptionalLocale(localeString)) {
final String layoutDisplayName = SubtypeLocale.getKeyboardLayoutSetDisplayName(
keyboardLayoutSetName);
- layoutDisplayNameExtraValue = StringUtils.appendToCsvIfNotExists(
+ layoutDisplayNameExtraValue = StringUtils.appendToCommaSplittableTextIfNotExists(
UNTRANSLATABLE_STRING_IN_SUBTYPE_NAME + "=" + layoutDisplayName, extraValue);
} else {
layoutDisplayNameExtraValue = extraValue;
}
- final String additionalSubtypeExtraValue = StringUtils.appendToCsvIfNotExists(
- IS_ADDITIONAL_SUBTYPE, layoutDisplayNameExtraValue);
+ final String additionalSubtypeExtraValue =
+ StringUtils.appendToCommaSplittableTextIfNotExists(
+ IS_ADDITIONAL_SUBTYPE, layoutDisplayNameExtraValue);
final int nameId = SubtypeLocale.getSubtypeNameId(localeString, keyboardLayoutSetName);
return new InputMethodSubtype(nameId, R.drawable.ic_subtype_keyboard,
localeString, KEYBOARD_MODE,
@@ -66,8 +67,9 @@ public final class AdditionalSubtype {
final String localeString = subtype.getLocale();
final String keyboardLayoutSetName = SubtypeLocale.getKeyboardLayoutSetName(subtype);
final String layoutExtraValue = KEYBOARD_LAYOUT_SET + "=" + keyboardLayoutSetName;
- final String extraValue = StringUtils.removeFromCsvIfExists(layoutExtraValue,
- StringUtils.removeFromCsvIfExists(IS_ADDITIONAL_SUBTYPE, subtype.getExtraValue()));
+ final String extraValue = StringUtils.removeFromCommaSplittableTextIfExists(
+ layoutExtraValue, StringUtils.removeFromCommaSplittableTextIfExists(
+ IS_ADDITIONAL_SUBTYPE, subtype.getExtraValue()));
final String basePrefSubtype = localeString + LOCALE_AND_LAYOUT_SEPARATOR
+ keyboardLayoutSetName;
return extraValue.isEmpty() ? basePrefSubtype
diff --git a/java/src/com/android/inputmethod/latin/AssetFileAddress.java b/java/src/com/android/inputmethod/latin/AssetFileAddress.java
index 47c750f54..875192554 100644
--- a/java/src/com/android/inputmethod/latin/AssetFileAddress.java
+++ b/java/src/com/android/inputmethod/latin/AssetFileAddress.java
@@ -24,7 +24,7 @@ import java.io.File;
* the package file. Open it correctly thus requires the name of the package it is in, but
* also the offset in the file and the length of this data. This class encapsulates these three.
*/
-final class AssetFileAddress {
+public final class AssetFileAddress {
public final String mFilename;
public final long mOffset;
public final long mLength;
diff --git a/java/src/com/android/inputmethod/latin/AutoCorrection.java b/java/src/com/android/inputmethod/latin/AutoCorrection.java
index fa35922b0..86be4295a 100644
--- a/java/src/com/android/inputmethod/latin/AutoCorrection.java
+++ b/java/src/com/android/inputmethod/latin/AutoCorrection.java
@@ -32,12 +32,13 @@ public final class AutoCorrection {
// Purely static class: can't instantiate.
}
- public static boolean isValidWord(final ConcurrentHashMap<String, Dictionary> dictionaries,
- final String word, final boolean ignoreCase) {
+ public static boolean isValidWord(final Suggest suggest, final String word,
+ final boolean ignoreCase) {
if (TextUtils.isEmpty(word)) {
return false;
}
- final String lowerCasedWord = word.toLowerCase();
+ final ConcurrentHashMap<String, Dictionary> dictionaries = suggest.getUnigramDictionaries();
+ final String lowerCasedWord = word.toLowerCase(suggest.mLocale);
for (final String key : dictionaries.keySet()) {
final Dictionary dictionary = dictionaries.get(key);
// It's unclear how realistically 'dictionary' can be null, but the monkey is somehow
@@ -73,13 +74,6 @@ public final class AutoCorrection {
return maxFreq;
}
- // Returns true if this is in any of the dictionaries.
- public static boolean isInTheDictionary(
- final ConcurrentHashMap<String, Dictionary> dictionaries,
- final String word, final boolean ignoreCase) {
- return isValidWord(dictionaries, word, ignoreCase);
- }
-
public static boolean suggestionExceedsAutoCorrectionThreshold(
final SuggestedWordInfo suggestion, final String consideredWord,
final float autoCorrectionThreshold) {
diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionary.java b/java/src/com/android/inputmethod/latin/BinaryDictionary.java
index 4fc1919dc..aad129d76 100644
--- a/java/src/com/android/inputmethod/latin/BinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/BinaryDictionary.java
@@ -45,7 +45,7 @@ public final class BinaryDictionary extends Dictionary {
private final int[] mOutputScores = new int[MAX_RESULTS];
private final int[] mOutputTypes = new int[MAX_RESULTS];
- private final boolean mUseFullEditDistance;
+ private final NativeSuggestOptions mNativeSuggestOptions = new NativeSuggestOptions();
private final SparseArray<DicTraverseSession> mDicTraverseSessions =
CollectionUtils.newSparseArray();
@@ -79,7 +79,7 @@ public final class BinaryDictionary extends Dictionary {
final boolean useFullEditDistance, final Locale locale, final String dictType) {
super(dictType);
mLocale = locale;
- mUseFullEditDistance = useFullEditDistance;
+ mNativeSuggestOptions.setUseFullEditDistance(useFullEditDistance);
loadDictionary(filename, offset, length);
}
@@ -94,7 +94,7 @@ public final class BinaryDictionary extends Dictionary {
private static native int getSuggestionsNative(long dict, long proximityInfo,
long traverseSession, int[] xCoordinates, int[] yCoordinates, int[] times,
int[] pointerIds, int[] inputCodePoints, int inputSize, int commitPoint,
- boolean isGesture, int[] prevWordCodePointArray, boolean useFullEditDistance,
+ int[] suggestOptions, int[] prevWordCodePointArray,
int[] outputCodePoints, int[] outputScores, int[] outputIndices, int[] outputTypes);
private static native float calcNormalizedScoreNative(int[] before, int[] after, int score);
private static native int editDistanceNative(int[] before, int[] after);
@@ -135,12 +135,15 @@ public final class BinaryDictionary extends Dictionary {
final InputPointers ips = composer.getInputPointers();
final int inputSize = isGesture ? ips.getPointerSize() : composerSize;
+ mNativeSuggestOptions.setIsGesture(isGesture);
+ mNativeSuggestOptions.setAdditionalFeaturesOptions(
+ AdditionalFeaturesSettingUtils.getAdditionalNativeSuggestOptions());
// proximityInfo and/or prevWordForBigrams may not be null.
final int count = getSuggestionsNative(mNativeDict, proximityInfo.getNativeProximityInfo(),
getTraverseSession(sessionId).getSession(), ips.getXCoordinates(),
ips.getYCoordinates(), ips.getTimes(), ips.getPointerIds(), mInputCodePoints,
- inputSize, 0 /* commitPoint */, isGesture, prevWordCodePointArray,
- mUseFullEditDistance, mOutputCodePoints, mOutputScores, mSpaceIndices,
+ inputSize, 0 /* commitPoint */, mNativeSuggestOptions.getOptions(),
+ prevWordCodePointArray, mOutputCodePoints, mOutputScores, mSpaceIndices,
mOutputTypes);
final ArrayList<SuggestedWordInfo> suggestions = CollectionUtils.newArrayList();
for (int j = 0; j < count; ++j) {
diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java b/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java
index a9b58de44..c038db87c 100644
--- a/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java
+++ b/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java
@@ -32,6 +32,7 @@ import com.android.inputmethod.latin.DictionaryInfoUtils.DictionaryInfo;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
+import java.io.Closeable;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
@@ -319,20 +320,12 @@ public final class BinaryDictionaryFileDumper {
// Try the next method.
} finally {
// Ignore exceptions while closing files.
- try {
- if (null != afd) afd.close();
- if (null != inputStream) inputStream.close();
- if (null != uncompressedStream) uncompressedStream.close();
- if (null != decryptedStream) decryptedStream.close();
- if (null != bufferedInputStream) bufferedInputStream.close();
- } catch (Exception e) {
- Log.e(TAG, "Exception while closing a file descriptor", e);
- }
- try {
- if (null != bufferedOutputStream) bufferedOutputStream.close();
- } catch (Exception e) {
- Log.e(TAG, "Exception while closing a file", e);
- }
+ closeAssetFileDescriptorAndReportAnyException(afd);
+ closeCloseableAndReportAnyException(inputStream);
+ closeCloseableAndReportAnyException(uncompressedStream);
+ closeCloseableAndReportAnyException(decryptedStream);
+ closeCloseableAndReportAnyException(bufferedInputStream);
+ closeCloseableAndReportAnyException(bufferedOutputStream);
}
}
@@ -352,6 +345,26 @@ public final class BinaryDictionaryFileDumper {
}
}
+ // Ideally the two following methods should be merged, but AssetFileDescriptor does not
+ // implement Closeable although it does implement #close(), and Java does not have
+ // structural typing.
+ private static void closeAssetFileDescriptorAndReportAnyException(
+ final AssetFileDescriptor file) {
+ try {
+ if (null != file) file.close();
+ } catch (Exception e) {
+ Log.e(TAG, "Exception while closing a file", e);
+ }
+ }
+
+ private static void closeCloseableAndReportAnyException(final Closeable file) {
+ try {
+ if (null != file) file.close();
+ } catch (Exception e) {
+ Log.e(TAG, "Exception while closing a file", e);
+ }
+ }
+
/**
* Queries a content provider for word list data for some locale and cache the returned files
*
@@ -363,8 +376,14 @@ public final class BinaryDictionaryFileDumper {
*/
public static void cacheWordListsFromContentProvider(final Locale locale,
final Context context, final boolean hasDefaultWordList) {
- final ContentProviderClient providerClient = context.getContentResolver().
+ final ContentProviderClient providerClient;
+ try {
+ providerClient = context.getContentResolver().
acquireContentProviderClient(getProviderUriBuilder("").build());
+ } catch (final SecurityException e) {
+ Log.e(TAG, "No permission to communicate with the dictionary provider", e);
+ return;
+ }
if (null == providerClient) {
Log.e(TAG, "Can't establish communication with the dictionary provider");
return;
diff --git a/java/src/com/android/inputmethod/latin/Constants.java b/java/src/com/android/inputmethod/latin/Constants.java
index 86bb25562..bb4a42ede 100644
--- a/java/src/com/android/inputmethod/latin/Constants.java
+++ b/java/src/com/android/inputmethod/latin/Constants.java
@@ -172,22 +172,23 @@ public final class Constants {
/**
* Special keys code. Must be negative.
- * These should be aligned with KeyboardCodesSet.ID_TO_NAME[],
- * KeyboardCodesSet.DEFAULT[] and KeyboardCodesSet.RTL[]
+ * These should be aligned with {@link KeyboardCodesSet#ID_TO_NAME},
+ * {@link KeyboardCodesSet#DEFAULT}, and {@link KeyboardCodesSet#RTL}.
*/
public static final int CODE_SHIFT = -1;
- public static final int CODE_SWITCH_ALPHA_SYMBOL = -2;
- public static final int CODE_OUTPUT_TEXT = -3;
- public static final int CODE_DELETE = -4;
- public static final int CODE_SETTINGS = -5;
- public static final int CODE_SHORTCUT = -6;
- public static final int CODE_ACTION_NEXT = -7;
- public static final int CODE_ACTION_PREVIOUS = -8;
- public static final int CODE_LANGUAGE_SWITCH = -9;
- public static final int CODE_RESEARCH = -10;
- public static final int CODE_SHIFT_ENTER = -11;
+ public static final int CODE_CAPSLOCK = -2;
+ public static final int CODE_SWITCH_ALPHA_SYMBOL = -3;
+ public static final int CODE_OUTPUT_TEXT = -4;
+ public static final int CODE_DELETE = -5;
+ public static final int CODE_SETTINGS = -6;
+ public static final int CODE_SHORTCUT = -7;
+ public static final int CODE_ACTION_NEXT = -8;
+ public static final int CODE_ACTION_PREVIOUS = -9;
+ public static final int CODE_LANGUAGE_SWITCH = -10;
+ public static final int CODE_RESEARCH = -11;
+ public static final int CODE_SHIFT_ENTER = -12;
// Code value representing the code is not specified.
- public static final int CODE_UNSPECIFIED = -12;
+ public static final int CODE_UNSPECIFIED = -13;
public static boolean isLetterCode(final int code) {
return code >= CODE_SPACE;
@@ -196,6 +197,7 @@ public final class Constants {
public static String printableCode(final int code) {
switch (code) {
case CODE_SHIFT: return "shift";
+ case CODE_CAPSLOCK: return "capslock";
case CODE_SWITCH_ALPHA_SYMBOL: return "symbol";
case CODE_OUTPUT_TEXT: return "text";
case CODE_DELETE: return "delete";
@@ -215,10 +217,6 @@ public final class Constants {
}
}
- // Constants for CSV parsing.
- public static final char CSV_SEPARATOR = ',';
- public static final char CSV_ESCAPE = '\\';
-
private Constants() {
// This utility class is not publicly instantiable.
}
diff --git a/java/src/com/android/inputmethod/latin/DictionaryFactory.java b/java/src/com/android/inputmethod/latin/DictionaryFactory.java
index 40e51672a..4514ec2ec 100644
--- a/java/src/com/android/inputmethod/latin/DictionaryFactory.java
+++ b/java/src/com/android/inputmethod/latin/DictionaryFactory.java
@@ -21,6 +21,8 @@ import android.content.res.AssetFileDescriptor;
import android.content.res.Resources;
import android.util.Log;
+import com.android.inputmethod.annotations.UsedForTesting;
+
import java.io.File;
import java.util.ArrayList;
import java.util.LinkedList;
@@ -126,21 +128,22 @@ public final class DictionaryFactory {
/**
* Create a dictionary from passed data. This is intended for unit tests only.
- * @param dictionary the file to read
- * @param startOffset the offset in the file where the data starts
- * @param length the length of the data
+ * @param dictionaryList the list of files to read, with their offsets and lengths
* @param useFullEditDistance whether to use the full edit distance in suggestions
* @return the created dictionary, or null.
*/
- public static Dictionary createDictionaryForTest(File dictionary, long startOffset, long length,
+ @UsedForTesting
+ public static Dictionary createDictionaryForTest(final AssetFileAddress[] dictionaryList,
final boolean useFullEditDistance, Locale locale) {
- if (dictionary.isFile()) {
- return new BinaryDictionary(dictionary.getAbsolutePath(), startOffset, length,
- useFullEditDistance, locale, Dictionary.TYPE_MAIN);
- } else {
- Log.e(TAG, "Could not find the file. path=" + dictionary.getAbsolutePath());
- return null;
+ final DictionaryCollection dictionaryCollection =
+ new DictionaryCollection(Dictionary.TYPE_MAIN);
+ for (final AssetFileAddress address : dictionaryList) {
+ final BinaryDictionary binaryDictionary = new BinaryDictionary(address.mFilename,
+ address.mOffset, address.mLength, useFullEditDistance, locale,
+ Dictionary.TYPE_MAIN);
+ dictionaryCollection.addDictionary(binaryDictionary);
}
+ return dictionaryCollection;
}
/**
diff --git a/java/src/com/android/inputmethod/latin/DictionaryInfoUtils.java b/java/src/com/android/inputmethod/latin/DictionaryInfoUtils.java
index df7bad8d0..9d478491a 100644
--- a/java/src/com/android/inputmethod/latin/DictionaryInfoUtils.java
+++ b/java/src/com/android/inputmethod/latin/DictionaryInfoUtils.java
@@ -30,6 +30,7 @@ import com.android.inputmethod.latin.makedict.UnsupportedFormatException;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
+import java.util.Iterator;
import java.util.Locale;
/**
@@ -301,12 +302,14 @@ public class DictionaryInfoUtils {
private static void addOrUpdateDictInfo(final ArrayList<DictionaryInfo> dictList,
final DictionaryInfo newElement) {
- for (final DictionaryInfo info : dictList) {
- if (info.mLocale.equals(newElement.mLocale)) {
- if (newElement.mVersion <= info.mVersion) {
+ final Iterator<DictionaryInfo> iter = dictList.iterator();
+ while (iter.hasNext()) {
+ final DictionaryInfo thisDictInfo = iter.next();
+ if (thisDictInfo.mLocale.equals(newElement.mLocale)) {
+ if (newElement.mVersion <= thisDictInfo.mVersion) {
return;
}
- dictList.remove(info);
+ iter.remove();
}
}
dictList.add(newElement);
diff --git a/java/src/com/android/inputmethod/latin/InputAttributes.java b/java/src/com/android/inputmethod/latin/InputAttributes.java
index dd58db575..1f673e9b0 100644
--- a/java/src/com/android/inputmethod/latin/InputAttributes.java
+++ b/java/src/com/android/inputmethod/latin/InputAttributes.java
@@ -199,6 +199,6 @@ public final class InputAttributes {
if (editorInfo == null) return false;
final String findingKey = (packageName != null) ? packageName + "." + key
: key;
- return StringUtils.containsInCsv(findingKey, editorInfo.privateImeOptions);
+ return StringUtils.containsInCommaSplittableText(findingKey, editorInfo.privateImeOptions);
}
}
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index c464a7067..0bf167fd4 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -480,6 +480,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
final InputAttributes inputAttributes =
new InputAttributes(getCurrentInputEditorInfo(), isFullscreenMode());
mSettings.loadSettings(locale, inputAttributes);
+ AudioAndHapticFeedbackManager.getInstance().onSettingsChanged(mSettings.getCurrent());
// May need to reset the contacts dictionary depending on the user settings.
resetContactsDictionary(null == mSuggest ? null : mSuggest.getContactsDictionary());
}
@@ -854,6 +855,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
}
// Remove pending messages related to update suggestions
mHandler.cancelUpdateSuggestionStrip();
+ if (mWordComposer.isComposingWord()) mConnection.finishComposingText();
resetComposingState(true /* alsoResetLastComposedWord */);
// Notify ResearchLogger
if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
@@ -1418,8 +1420,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
LatinImeLogger.logOnDelete(x, y);
break;
case Constants.CODE_SHIFT:
- // Note: calling back to the keyboard on Shift key is handled in onPressKey()
- // and onReleaseKey().
+ // Note: Calling back to the keyboard on Shift key is handled in
+ // {@link #onPressKey(int,boolean)} and {@link #onReleaseKey(int,boolean)}.
final Keyboard currentKeyboard = switcher.getKeyboard();
if (null != currentKeyboard && currentKeyboard.mId.isAlphabetKeyboard()) {
// TODO: Instead of checking for alphabetic keyboard here, separate keycodes for
@@ -1427,9 +1429,13 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
handleRecapitalize();
}
break;
+ case Constants.CODE_CAPSLOCK:
+ // Note: Changing keyboard to shift lock state is handled in
+ // {@link KeyboardSwitcher#onCodeInput(int)}.
+ break;
case Constants.CODE_SWITCH_ALPHA_SYMBOL:
- // Note: calling back to the keyboard on symbol key is handled in onPressKey()
- // and onReleaseKey().
+ // Note: Calling back to the keyboard on symbol key is handled in
+ // {@link #onPressKey(int,boolean)} and {@link #onReleaseKey(int,boolean)}.
break;
case Constants.CODE_SETTINGS:
onSettingsKeyPressed();
@@ -1482,8 +1488,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
break;
}
switcher.onCodeInput(primaryCode);
- // Reset after any single keystroke, except shift and symbol-shift
+ // Reset after any single keystroke, except shift, capslock, and symbol-shift
if (!didAutoCorrect && primaryCode != Constants.CODE_SHIFT
+ && primaryCode != Constants.CODE_CAPSLOCK
&& primaryCode != Constants.CODE_SWITCH_ALPHA_SYMBOL)
mLastComposedWord.deactivate();
if (Constants.CODE_DELETE != primaryCode) {
@@ -1635,8 +1642,10 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
public void onStartBatchInput(final LatinIME latinIme) {
synchronized (mLock) {
mHandler.removeMessages(MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP);
- mLatinIme = latinIme;
mInBatchInput = true;
+ mLatinIme = latinIme;
+ mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip(
+ SuggestedWords.EMPTY, false /* dismissGestureFloatingPreviewText */);
}
}
@@ -1795,8 +1804,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
final String word = mWordComposer.getTypedWord();
ResearchLogger.latinIME_handleBackspace_batch(word, 1);
- ResearchLogger.getInstance().uncommitCurrentLogUnit(
- word, false /* dumpCurrentLogUnit */);
}
final String rejectedSuggestion = mWordComposer.getTypedWord();
mWordComposer.reset();
@@ -1823,6 +1830,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
// like the smiley key or the .com key.
final int length = mEnteredText.length();
mConnection.deleteSurroundingText(length, 0);
+ if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
+ ResearchLogger.latinIME_handleBackspace_cancelTextInput(mEnteredText);
+ }
mEnteredText = null;
// If we have mEnteredText, then we know that mHasUncommittedTypedChars == false.
// In addition we know that spaceState is false, and that we should not be
@@ -1856,7 +1866,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
mLastSelectionEnd = mLastSelectionStart;
mConnection.deleteSurroundingText(numCharsDeleted, 0);
if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
- ResearchLogger.latinIME_handleBackspace(numCharsDeleted);
+ ResearchLogger.latinIME_handleBackspace(numCharsDeleted,
+ false /* shouldUncommitLogUnit */);
}
} else {
// There is no selection, just delete one character.
@@ -1874,12 +1885,13 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
mConnection.deleteSurroundingText(1, 0);
}
if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
- ResearchLogger.latinIME_handleBackspace(1);
+ ResearchLogger.latinIME_handleBackspace(1, true /* shouldUncommitLogUnit */);
}
if (mDeleteCount > DELETE_ACCELERATE_AT) {
mConnection.deleteSurroundingText(1, 0);
if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
- ResearchLogger.latinIME_handleBackspace(1);
+ ResearchLogger.latinIME_handleBackspace(1,
+ true /* shouldUncommitLogUnit */);
}
}
}
@@ -2020,9 +2032,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
// Returns true if we did an autocorrection, false otherwise.
private boolean handleSeparator(final int primaryCode, final int x, final int y,
final int spaceState) {
- if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
- ResearchLogger.latinIME_handleSeparator(primaryCode, mWordComposer.isComposingWord());
- }
boolean didAutoCorrect = false;
if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) {
// If we are in the middle of a recorrection, we need to commit the recorrection
@@ -2046,6 +2055,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
mSettings.getCurrent().isUsuallyPrecededBySpace(primaryCode)) {
promotePhantomSpace();
}
+ if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
+ ResearchLogger.latinIME_handleSeparator(primaryCode, mWordComposer.isComposingWord());
+ }
sendKeyCodePoint(primaryCode);
if (Constants.CODE_SPACE == primaryCode) {
@@ -2368,9 +2380,11 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
// Please note that if mSuggest is null, it means that everything is off: suggestion
// and correction, so we shouldn't try to show the hint
final boolean showingAddToDictionaryHint =
- SuggestedWordInfo.KIND_TYPED == suggestionInfo.mKind && mSuggest != null
- // If the suggestion is not in the dictionary, the hint should be shown.
- && !AutoCorrection.isValidWord(mSuggest.getUnigramDictionaries(), suggestion, true);
+ (SuggestedWordInfo.KIND_TYPED == suggestionInfo.mKind
+ || SuggestedWordInfo.KIND_OOV_CORRECTION == suggestionInfo.mKind)
+ && mSuggest != null
+ // If the suggestion is not in the dictionary, the hint should be shown.
+ && !AutoCorrection.isValidWord(mSuggest, suggestion, true);
if (mSettings.isInternal()) {
Stats.onSeparator((char)Constants.CODE_SPACE,
@@ -2585,8 +2599,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
ResearchLogger.latinIME_revertCommit(committedWord, originallyTypedWord,
mWordComposer.isBatchMode(), mLastComposedWord.mSeparatorString);
- ResearchLogger.getInstance().uncommitCurrentLogUnit(committedWord,
- true /* dumpCurrentLogUnit */);
}
// Don't restart suggestion yet. We'll restart if the user deletes the
// separator.
@@ -2599,10 +2611,10 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
public void promotePhantomSpace() {
if (mSettings.getCurrent().shouldInsertSpacesAutomatically()
&& !mConnection.textBeforeCursorLooksLikeURL()) {
- sendKeyCodePoint(Constants.CODE_SPACE);
if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
ResearchLogger.latinIME_promotePhantomSpace();
}
+ sendKeyCodePoint(Constants.CODE_SPACE);
}
}
@@ -2703,7 +2715,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
if (action.equals(ConnectivityManager.CONNECTIVITY_ACTION)) {
mSubtypeSwitcher.onNetworkStateChanged(intent);
} else if (action.equals(AudioManager.RINGER_MODE_CHANGED_ACTION)) {
- mKeyboardSwitcher.onRingerModeChanged();
+ AudioAndHapticFeedbackManager.getInstance().onRingerModeChanged();
}
}
};
diff --git a/java/src/com/android/inputmethod/latin/LocaleUtils.java b/java/src/com/android/inputmethod/latin/LocaleUtils.java
index 5fde8158a..a1e40502e 100644
--- a/java/src/com/android/inputmethod/latin/LocaleUtils.java
+++ b/java/src/com/android/inputmethod/latin/LocaleUtils.java
@@ -148,7 +148,7 @@ public final class LocaleUtils {
public static String getMatchLevelSortedString(int matchLevel) {
// This works because the match levels are 0~99 (actually 0~30)
// Ideally this should use a number of digits equals to the 1og10 of the greater matchLevel
- return String.format("%02d", MATCH_LEVEL_MAX - matchLevel);
+ return String.format(Locale.ROOT, "%02d", MATCH_LEVEL_MAX - matchLevel);
}
/**
diff --git a/java/src/com/android/inputmethod/latin/NativeSuggestOptions.java b/java/src/com/android/inputmethod/latin/NativeSuggestOptions.java
new file mode 100644
index 000000000..291551301
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/NativeSuggestOptions.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.inputmethod.latin;
+
+public class NativeSuggestOptions {
+ // Need to update suggest_options.h when you add, remove or reorder options.
+ private static final int IS_GESTURE = 0;
+ private static final int USE_FULL_EDIT_DISTANCE = 1;
+ private static final int OPTIONS_SIZE = 2;
+
+ private final int[] mOptions = new int[OPTIONS_SIZE
+ + AdditionalFeaturesSettingUtils.ADDITIONAL_FEATURES_SETTINGS_SIZE];
+
+ public void setIsGesture(final boolean value) {
+ setBooleanOption(IS_GESTURE, value);
+ }
+
+ public void setUseFullEditDistance(final boolean value) {
+ setBooleanOption(USE_FULL_EDIT_DISTANCE, value);
+ }
+
+ public void setAdditionalFeaturesOptions(final int[] additionalOptions) {
+ for (int i = 0; i < additionalOptions.length; i++) {
+ setIntegerOption(OPTIONS_SIZE + i, additionalOptions[i]);
+ }
+ }
+
+ public int[] getOptions() {
+ return mOptions;
+ }
+
+ private void setBooleanOption(final int key, final boolean value) {
+ mOptions[key] = value ? 1 : 0;
+ }
+
+ private void setIntegerOption(final int key, final int value) {
+ mOptions[key] = value;
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/ResourceUtils.java b/java/src/com/android/inputmethod/latin/ResourceUtils.java
index a9fba5348..0eb8b4f09 100644
--- a/java/src/com/android/inputmethod/latin/ResourceUtils.java
+++ b/java/src/com/android/inputmethod/latin/ResourceUtils.java
@@ -27,6 +27,7 @@ import com.android.inputmethod.annotations.UsedForTesting;
import java.util.ArrayList;
import java.util.HashMap;
+import java.util.regex.PatternSyntaxException;
public final class ResourceUtils {
private static final String TAG = ResourceUtils.class.getSimpleName();
@@ -83,22 +84,39 @@ public final class ResourceUtils {
return overrideValue;
}
- final String defaultValue = findDefaultConstant(overrideArray);
- // The defaultValue might be an empty string.
- if (defaultValue == null) {
- Log.w(TAG, "Couldn't find override value nor default value:"
- + " resource="+ res.getResourceEntryName(overrideResId)
- + " build=" + sBuildKeyValuesDebugString);
- } else {
- Log.i(TAG, "Found default value:"
- + " resource="+ res.getResourceEntryName(overrideResId)
- + " build=" + sBuildKeyValuesDebugString
- + " default=" + defaultValue);
+ String defaultValue = null;
+ try {
+ defaultValue = findDefaultConstant(overrideArray);
+ // The defaultValue might be an empty string.
+ if (defaultValue == null) {
+ Log.w(TAG, "Couldn't find override value nor default value:"
+ + " resource="+ res.getResourceEntryName(overrideResId)
+ + " build=" + sBuildKeyValuesDebugString);
+ } else {
+ Log.i(TAG, "Found default value:"
+ + " resource="+ res.getResourceEntryName(overrideResId)
+ + " build=" + sBuildKeyValuesDebugString
+ + " default=" + defaultValue);
+ }
+ } catch (final DeviceOverridePatternSyntaxError e) {
+ Log.w(TAG, "Syntax error, ignored", e);
}
sDeviceOverrideValueMap.put(key, defaultValue);
return defaultValue;
}
+ @SuppressWarnings("serial")
+ static class DeviceOverridePatternSyntaxError extends Exception {
+ public DeviceOverridePatternSyntaxError(final String message, final String expression) {
+ this(message, expression, null);
+ }
+
+ public DeviceOverridePatternSyntaxError(final String message, final String expression,
+ final Throwable throwable) {
+ super(message + ": " + expression, throwable);
+ }
+ }
+
/**
* Find the condition that fulfills specified key value pairs from an array of
* "condition,constant", and return the corresponding string constant. A condition is
@@ -123,10 +141,12 @@ public final class ResourceUtils {
if (conditionConstantArray == null || keyValuePairs == null) {
return null;
}
+ String foundValue = null;
for (final String conditionConstant : conditionConstantArray) {
final int posComma = conditionConstant.indexOf(',');
if (posComma < 0) {
- throw new RuntimeException("Array element has no comma: " + conditionConstant);
+ Log.w(TAG, "Array element has no comma: " + conditionConstant);
+ continue;
}
final String condition = conditionConstant.substring(0, posComma);
if (condition.isEmpty()) {
@@ -134,44 +154,59 @@ public final class ResourceUtils {
// {@link #findConstantForDefault(String[])}.
continue;
}
- if (fulfillsCondition(keyValuePairs, condition)) {
- return conditionConstant.substring(posComma + 1);
+ try {
+ if (fulfillsCondition(keyValuePairs, condition)) {
+ // Take first match
+ if (foundValue == null) {
+ foundValue = conditionConstant.substring(posComma + 1);
+ }
+ // And continue walking through all conditions.
+ }
+ } catch (final DeviceOverridePatternSyntaxError e) {
+ Log.w(TAG, "Syntax error, ignored", e);
}
}
- return null;
+ return foundValue;
}
private static boolean fulfillsCondition(final HashMap<String,String> keyValuePairs,
- final String condition) {
+ final String condition) throws DeviceOverridePatternSyntaxError {
final String[] patterns = condition.split(":");
// Check all patterns in a condition are true
+ boolean matchedAll = true;
for (final String pattern : patterns) {
final int posEqual = pattern.indexOf('=');
if (posEqual < 0) {
- throw new RuntimeException("Pattern has no '=': " + condition);
+ throw new DeviceOverridePatternSyntaxError("Pattern has no '='", condition);
}
final String key = pattern.substring(0, posEqual);
final String value = keyValuePairs.get(key);
if (value == null) {
- throw new RuntimeException("Found unknown key: " + condition);
+ throw new DeviceOverridePatternSyntaxError("Unknown key", condition);
}
final String patternRegexpValue = pattern.substring(posEqual + 1);
- if (!value.matches(patternRegexpValue)) {
- return false;
+ try {
+ if (!value.matches(patternRegexpValue)) {
+ matchedAll = false;
+ // And continue walking through all patterns.
+ }
+ } catch (final PatternSyntaxException e) {
+ throw new DeviceOverridePatternSyntaxError("Syntax error", condition, e);
}
}
- return true;
+ return matchedAll;
}
@UsedForTesting
- static String findDefaultConstant(final String[] conditionConstantArray) {
+ static String findDefaultConstant(final String[] conditionConstantArray)
+ throws DeviceOverridePatternSyntaxError {
if (conditionConstantArray == null) {
return null;
}
for (final String condition : conditionConstantArray) {
final int posComma = condition.indexOf(',');
if (posComma < 0) {
- throw new RuntimeException("Array element has no comma: " + condition);
+ throw new DeviceOverridePatternSyntaxError("Array element has no comma", condition);
}
if (posComma == 0) { // condition is empty.
return condition.substring(posComma + 1);
diff --git a/java/src/com/android/inputmethod/latin/SeekBarDialogPreference.java b/java/src/com/android/inputmethod/latin/SeekBarDialogPreference.java
index 7c4156c48..3ea9fedd7 100644
--- a/java/src/com/android/inputmethod/latin/SeekBarDialogPreference.java
+++ b/java/src/com/android/inputmethod/latin/SeekBarDialogPreference.java
@@ -32,6 +32,7 @@ public final class SeekBarDialogPreference extends DialogPreference
public int readValue(final String key);
public int readDefaultValue(final String key);
public void writeValue(final int value, final String key);
+ public void writeDefaultValue(final String key);
public void feedbackValue(final int value);
}
@@ -122,12 +123,16 @@ public final class SeekBarDialogPreference extends DialogPreference
@Override
public void onClick(final DialogInterface dialog, final int which) {
super.onClick(dialog, which);
+ final String key = getKey();
if (which == DialogInterface.BUTTON_NEUTRAL) {
- setValue(clipValue(mValueProxy.readDefaultValue(getKey())), false /* fromUser */);
+ setValue(clipValue(mValueProxy.readDefaultValue(key)), false /* fromUser */);
+ mValueProxy.writeDefaultValue(key);
+ return;
}
- if (which != DialogInterface.BUTTON_NEGATIVE) {
+ if (which == DialogInterface.BUTTON_POSITIVE) {
setSummary(mValueView.getText());
- mValueProxy.writeValue(getClippedValueFromProgress(mSeekBar.getProgress()), getKey());
+ mValueProxy.writeValue(getClippedValueFromProgress(mSeekBar.getProgress()), key);
+ return;
}
}
diff --git a/java/src/com/android/inputmethod/latin/Settings.java b/java/src/com/android/inputmethod/latin/Settings.java
index 9fefb58a6..a6149c6ec 100644
--- a/java/src/com/android/inputmethod/latin/Settings.java
+++ b/java/src/com/android/inputmethod/latin/Settings.java
@@ -21,6 +21,7 @@ import android.content.SharedPreferences;
import android.content.pm.ApplicationInfo;
import android.content.res.Resources;
import android.preference.PreferenceManager;
+import android.util.Log;
import com.android.inputmethod.latin.LocaleUtils.RunInLocale;
@@ -28,6 +29,7 @@ import java.util.HashMap;
import java.util.Locale;
public final class Settings implements SharedPreferences.OnSharedPreferenceChangeListener {
+ private static final String TAG = Settings.class.getSimpleName();
// In the same order as xml/prefs.xml
public static final String PREF_GENERAL_SETTINGS = "general_settings";
public static final String PREF_AUTO_CAP = "auto_cap";
@@ -114,6 +116,12 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
@Override
public void onSharedPreferenceChanged(final SharedPreferences prefs, final String key) {
+ if (mSettingsValues == null) {
+ // TODO: Introduce a static function to register this class and ensure that
+ // loadSettings must be called before "onSharedPreferenceChanged" is called.
+ Log.w(TAG, "onSharedPreferenceChanged called before loadSettings.");
+ return;
+ }
loadSettings(mCurrentLocale, mSettingsValues.mInputAttributes);
}
diff --git a/java/src/com/android/inputmethod/latin/SettingsFragment.java b/java/src/com/android/inputmethod/latin/SettingsFragment.java
index 835ef7b46..7225cd6bf 100644
--- a/java/src/com/android/inputmethod/latin/SettingsFragment.java
+++ b/java/src/com/android/inputmethod/latin/SettingsFragment.java
@@ -207,6 +207,8 @@ public final class SettingsFragment extends InputMethodSettingsFragment
if (!Settings.readFromBuildConfigIfGestureInputEnabled(res)) {
removePreference(Settings.PREF_GESTURE_SETTINGS, getPreferenceScreen());
+ } else {
+ AdditionalFeaturesSettingUtils.addAdditionalFeaturesPreferences(context, this);
}
setupKeyLongpressTimeoutSettings(prefs, res);
@@ -327,6 +329,11 @@ public final class SettingsFragment extends InputMethodSettingsFragment
}
@Override
+ public void writeDefaultValue(final String key) {
+ sp.edit().remove(key).apply();
+ }
+
+ @Override
public int readValue(final String key) {
return Settings.readKeypressVibrationDuration(sp, res);
}
@@ -357,6 +364,11 @@ public final class SettingsFragment extends InputMethodSettingsFragment
}
@Override
+ public void writeDefaultValue(final String key) {
+ sp.edit().remove(key).apply();
+ }
+
+ @Override
public int readValue(final String key) {
return Settings.readKeyLongpressTimeout(sp, res);
}
@@ -395,6 +407,11 @@ public final class SettingsFragment extends InputMethodSettingsFragment
}
@Override
+ public void writeDefaultValue(final String key) {
+ sp.edit().remove(key).apply();
+ }
+
+ @Override
public int readValue(final String key) {
return getPercentageFromValue(Settings.readKeypressSoundVolume(sp, res));
}
diff --git a/java/src/com/android/inputmethod/latin/SettingsValues.java b/java/src/com/android/inputmethod/latin/SettingsValues.java
index 615b2dfab..09102447f 100644
--- a/java/src/com/android/inputmethod/latin/SettingsValues.java
+++ b/java/src/com/android/inputmethod/latin/SettingsValues.java
@@ -80,6 +80,10 @@ public final class SettingsValues {
private final boolean mVoiceKeyEnabled;
private final boolean mVoiceKeyOnMain;
+ // Setting values for additional features
+ public final int[] mAdditionalFeaturesSettingValues =
+ new int[AdditionalFeaturesSettingUtils.ADDITIONAL_FEATURES_SETTINGS_SIZE];
+
// Debug settings
public final boolean mIsInternal;
@@ -96,7 +100,7 @@ public final class SettingsValues {
mWordConnectors =
StringUtils.toCodePointArray(res.getString(R.string.symbols_word_connectors));
Arrays.sort(mWordConnectors);
- final String[] suggestPuncsSpec = StringUtils.parseCsvString(res.getString(
+ final String[] suggestPuncsSpec = KeySpecParser.splitKeySpecs(res.getString(
R.string.suggested_punctuations));
mSuggestPuncList = createSuggestPuncList(suggestPuncsSpec);
mWordSeparators = res.getString(R.string.symbols_word_separators);
@@ -149,6 +153,8 @@ public final class SettingsValues {
Settings.PREF_SHOW_SUGGESTIONS_SETTING,
res.getString(R.string.prefs_suggestion_visibility_default_value));
mSuggestionVisibility = createSuggestionVisibility(res, showSuggestionsSetting);
+ AdditionalFeaturesSettingUtils.readAdditionalFeaturesPreferencesIntoArray(
+ prefs, mAdditionalFeaturesSettingValues);
mIsInternal = Settings.isInternal(prefs);
}
diff --git a/java/src/com/android/inputmethod/latin/StringUtils.java b/java/src/com/android/inputmethod/latin/StringUtils.java
index ab050d7a3..c2fd4fb32 100644
--- a/java/src/com/android/inputmethod/latin/StringUtils.java
+++ b/java/src/com/android/inputmethod/latin/StringUtils.java
@@ -35,33 +35,55 @@ public final class StringUtils {
return text.codePointCount(0, text.length());
}
- public static boolean containsInArray(final String key, final String[] array) {
+ public static boolean containsInArray(final String text, final String[] array) {
for (final String element : array) {
- if (key.equals(element)) return true;
+ if (text.equals(element)) return true;
}
return false;
}
- public static boolean containsInCsv(final String key, final String csv) {
- if (TextUtils.isEmpty(csv)) return false;
- return containsInArray(key, csv.split(","));
+ /**
+ * Comma-Splittable Text is similar to Comma-Separated Values (CSV) but has much simpler syntax.
+ * Unlike CSV, Comma-Splittable Text has no escaping mechanism, so that the text can't contain
+ * a comma character in it.
+ */
+ private static final String SEPARATOR_FOR_COMMA_SPLITTABLE_TEXT = ",";
+
+ public static boolean containsInCommaSplittableText(final String text,
+ final String extraValues) {
+ if (TextUtils.isEmpty(extraValues)) {
+ return false;
+ }
+ return containsInArray(text, extraValues.split(SEPARATOR_FOR_COMMA_SPLITTABLE_TEXT));
}
- public static String appendToCsvIfNotExists(final String key, final String csv) {
- if (TextUtils.isEmpty(csv)) return key;
- if (containsInCsv(key, csv)) return csv;
- return csv + "," + key;
+ public static String appendToCommaSplittableTextIfNotExists(final String text,
+ final String extraValues) {
+ if (TextUtils.isEmpty(extraValues)) {
+ return text;
+ }
+ if (containsInCommaSplittableText(text, extraValues)) {
+ return extraValues;
+ }
+ return extraValues + SEPARATOR_FOR_COMMA_SPLITTABLE_TEXT + text;
}
- public static String removeFromCsvIfExists(final String key, final String csv) {
- if (TextUtils.isEmpty(csv)) return "";
- final String[] elements = csv.split(",");
- if (!containsInArray(key, elements)) return csv;
+ public static String removeFromCommaSplittableTextIfExists(final String text,
+ final String extraValues) {
+ if (TextUtils.isEmpty(extraValues)) {
+ return "";
+ }
+ final String[] elements = extraValues.split(SEPARATOR_FOR_COMMA_SPLITTABLE_TEXT);
+ if (!containsInArray(text, elements)) {
+ return extraValues;
+ }
final ArrayList<String> result = CollectionUtils.newArrayList(elements.length - 1);
for (final String element : elements) {
- if (!key.equals(element)) result.add(element);
+ if (!text.equals(element)) {
+ result.add(element);
+ }
}
- return TextUtils.join(",", result);
+ return TextUtils.join(SEPARATOR_FOR_COMMA_SPLITTABLE_TEXT, result);
}
/**
@@ -131,44 +153,6 @@ public final class StringUtils {
return codePoints;
}
- public static String[] parseCsvString(final String text) {
- final int size = text.length();
- if (size == 0) {
- return null;
- }
- if (codePointCount(text) == 1) {
- return text.codePointAt(0) == Constants.CSV_SEPARATOR ? null : new String[] { text };
- }
-
- ArrayList<String> list = null;
- int start = 0;
- for (int pos = 0; pos < size; pos++) {
- final char c = text.charAt(pos);
- if (c == Constants.CSV_SEPARATOR) {
- // Skip empty entry.
- if (pos - start > 0) {
- if (list == null) {
- list = CollectionUtils.newArrayList();
- }
- list.add(text.substring(start, pos));
- }
- // Skip comma
- start = pos + 1;
- } else if (c == Constants.CSV_ESCAPE) {
- // Skip escape character and escaped character.
- pos++;
- }
- }
- final String remain = (size - start > 0) ? text.substring(start) : null;
- if (list == null) {
- return remain != null ? new String[] { remain } : null;
- }
- if (remain != null) {
- list.add(remain);
- }
- return list.toArray(new String[list.size()]);
- }
-
// This method assumes the text is not null. For the empty string, it returns CAPITALIZE_NONE.
public static int getCapitalizationType(final String text) {
// If the first char is not uppercase, then the word is either all lower case or
diff --git a/java/src/com/android/inputmethod/latin/Suggest.java b/java/src/com/android/inputmethod/latin/Suggest.java
index dc9bef22a..e783e6d51 100644
--- a/java/src/com/android/inputmethod/latin/Suggest.java
+++ b/java/src/com/android/inputmethod/latin/Suggest.java
@@ -23,7 +23,6 @@ import com.android.inputmethod.annotations.UsedForTesting;
import com.android.inputmethod.keyboard.ProximityInfo;
import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
-import java.io.File;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
@@ -77,9 +76,9 @@ public final class Suggest {
}
@UsedForTesting
- Suggest(final File dictionary, final long startOffset, final long length, final Locale locale) {
- final Dictionary mainDict = DictionaryFactory.createDictionaryForTest(dictionary,
- startOffset, length /* useFullEditDistance */, false, locale);
+ Suggest(final AssetFileAddress[] dictionaryList, final Locale locale) {
+ final Dictionary mainDict = DictionaryFactory.createDictionaryForTest(dictionaryList,
+ false /* useFullEditDistance */, locale);
mLocale = locale;
mMainDictionary = mainDict;
addOrReplaceDictionary(mDictionaries, Dictionary.TYPE_MAIN, mainDict);
@@ -229,7 +228,7 @@ public final class Suggest {
// or if it's a 2+ characters non-word (i.e. it's not in the dictionary).
final boolean allowsToBeAutoCorrected = (null != whitelistedWord
&& !whitelistedWord.equals(consideredWord))
- || (consideredWord.length() > 1 && !AutoCorrection.isInTheDictionary(mDictionaries,
+ || (consideredWord.length() > 1 && !AutoCorrection.isValidWord(this,
consideredWord, wordComposer.isFirstCharCapitalized()));
final boolean hasAutoCorrection;
@@ -379,7 +378,8 @@ public final class Suggest {
typedWord, cur.toString(), cur.mScore);
final String scoreInfoString;
if (normalizedScore > 0) {
- scoreInfoString = String.format("%d (%4.2f)", cur.mScore, normalizedScore);
+ scoreInfoString = String.format(
+ Locale.ROOT, "%d (%4.2f)", cur.mScore, normalizedScore);
} else {
scoreInfoString = Integer.toString(cur.mScore);
}
diff --git a/java/src/com/android/inputmethod/latin/SuggestedWords.java b/java/src/com/android/inputmethod/latin/SuggestedWords.java
index dfddb0ffe..7a16595a7 100644
--- a/java/src/com/android/inputmethod/latin/SuggestedWords.java
+++ b/java/src/com/android/inputmethod/latin/SuggestedWords.java
@@ -24,6 +24,9 @@ import java.util.Arrays;
import java.util.HashSet;
public final class SuggestedWords {
+ public static final int INDEX_OF_TYPED_WORD = 0;
+ public static final int INDEX_OF_AUTO_CORRECTION = 1;
+
private static final ArrayList<SuggestedWordInfo> EMPTY_WORD_INFO_LIST =
CollectionUtils.newArrayList(0);
public static final SuggestedWords EMPTY = new SuggestedWords(
@@ -61,12 +64,12 @@ public final class SuggestedWords {
return mSuggestedWordInfoList.size();
}
- public String getWord(int pos) {
- return mSuggestedWordInfoList.get(pos).mWord;
+ public String getWord(final int index) {
+ return mSuggestedWordInfoList.get(index).mWord;
}
- public SuggestedWordInfo getInfo(int pos) {
- return mSuggestedWordInfoList.get(pos);
+ public SuggestedWordInfo getInfo(final int index) {
+ return mSuggestedWordInfoList.get(index);
}
public boolean willAutoCorrect() {
@@ -108,8 +111,8 @@ public final class SuggestedWords {
SuggestedWordInfo.KIND_TYPED, Dictionary.TYPE_USER_TYPED));
alreadySeen.add(typedWord.toString());
final int previousSize = previousSuggestions.size();
- for (int pos = 1; pos < previousSize; pos++) {
- final SuggestedWordInfo prevWordInfo = previousSuggestions.getInfo(pos);
+ for (int index = 1; index < previousSize; index++) {
+ final SuggestedWordInfo prevWordInfo = previousSuggestions.getInfo(index);
final String prevWord = prevWordInfo.mWord;
// Filter out duplicate suggestion.
if (!alreadySeen.contains(prevWord)) {
@@ -132,7 +135,10 @@ public final class SuggestedWords {
public static final int KIND_APP_DEFINED = 6; // Suggested by the application
public static final int KIND_SHORTCUT = 7; // A shortcut
public static final int KIND_PREDICTION = 8; // A prediction (== a suggestion with no input)
- public static final int KIND_RESUMED = 9; // A resumed suggestion (comes from a span)
+ // KIND_RESUMED: A resumed suggestion (comes from a span, currently this type is used only
+ // in java for re-correction)
+ public static final int KIND_RESUMED = 9;
+ public static final int KIND_OOV_CORRECTION = 10; // Most probable string correction
public static final int KIND_MASK_FLAGS = 0xFFFFFF00; // Mask to get the flags
public static final int KIND_FLAG_POSSIBLY_OFFENSIVE = 0x80000000;
diff --git a/java/src/com/android/inputmethod/latin/Utils.java b/java/src/com/android/inputmethod/latin/Utils.java
index 0f96c54dc..949720fda 100644
--- a/java/src/com/android/inputmethod/latin/Utils.java
+++ b/java/src/com/android/inputmethod/latin/Utils.java
@@ -64,7 +64,8 @@ public final class Utils {
* task should be interrupted; otherwise, in-progress tasks are allowed
* to complete.
*/
- public static void cancelTask(AsyncTask<?, ?, ?> task, boolean mayInterruptIfRunning) {
+ public static void cancelTask(final AsyncTask<?, ?, ?> task,
+ final boolean mayInterruptIfRunning) {
if (task != null && task.getStatus() != AsyncTask.Status.FINISHED) {
task.cancel(mayInterruptIfRunning);
}
@@ -86,26 +87,34 @@ public final class Utils {
private RingCharBuffer() {
// Intentional empty constructor for singleton.
}
+
@UsedForTesting
public static RingCharBuffer getInstance() {
return sRingCharBuffer;
}
- public static RingCharBuffer init(InputMethodService context, boolean enabled,
- boolean usabilityStudy) {
- if (!(enabled || usabilityStudy)) return null;
+
+ public static RingCharBuffer init(final InputMethodService context, final boolean enabled,
+ final boolean usabilityStudy) {
+ if (!(enabled || usabilityStudy)) {
+ return null;
+ }
sRingCharBuffer.mContext = context;
sRingCharBuffer.mEnabled = true;
UsabilityStudyLogUtils.getInstance().init(context);
return sRingCharBuffer;
}
- private static int normalize(int in) {
+
+ private static int normalize(final int in) {
int ret = in % BUFSIZE;
return ret < 0 ? ret + BUFSIZE : ret;
}
+
// TODO: accept code points
@UsedForTesting
- public void push(char c, int x, int y) {
- if (!mEnabled) return;
+ public void push(final char c, final int x, final int y) {
+ if (!mEnabled) {
+ return;
+ }
mCharBuf[mEnd] = c;
mXBuf[mEnd] = x;
mYBuf[mEnd] = y;
@@ -114,52 +123,54 @@ public final class Utils {
++mLength;
}
}
+
public char pop() {
if (mLength < 1) {
return PLACEHOLDER_DELIMITER_CHAR;
- } else {
- mEnd = normalize(mEnd - 1);
- --mLength;
- return mCharBuf[mEnd];
}
+ mEnd = normalize(mEnd - 1);
+ --mLength;
+ return mCharBuf[mEnd];
}
- public char getBackwardNthChar(int n) {
+
+ public char getBackwardNthChar(final int n) {
if (mLength <= n || n < 0) {
return PLACEHOLDER_DELIMITER_CHAR;
- } else {
- return mCharBuf[normalize(mEnd - n - 1)];
}
+ return mCharBuf[normalize(mEnd - n - 1)];
}
- public int getPreviousX(char c, int back) {
- int index = normalize(mEnd - 2 - back);
+
+ public int getPreviousX(final char c, final int back) {
+ final int index = normalize(mEnd - 2 - back);
if (mLength <= back
|| Character.toLowerCase(c) != Character.toLowerCase(mCharBuf[index])) {
return INVALID_COORDINATE;
- } else {
- return mXBuf[index];
}
+ return mXBuf[index];
}
- public int getPreviousY(char c, int back) {
+
+ public int getPreviousY(final char c, final int back) {
int index = normalize(mEnd - 2 - back);
if (mLength <= back
|| Character.toLowerCase(c) != Character.toLowerCase(mCharBuf[index])) {
return INVALID_COORDINATE;
- } else {
- return mYBuf[index];
}
+ return mYBuf[index];
}
- public String getLastWord(int ignoreCharCount) {
- StringBuilder sb = new StringBuilder();
+
+ public String getLastWord(final int ignoreCharCount) {
+ final StringBuilder sb = new StringBuilder();
+ final LatinIME latinIme = (LatinIME)mContext;
int i = ignoreCharCount;
for (; i < mLength; ++i) {
- char c = mCharBuf[normalize(mEnd - 1 - i)];
- if (!((LatinIME)mContext).isWordSeparator(c)) {
+ final char c = mCharBuf[normalize(mEnd - 1 - i)];
+ if (!latinIme.isWordSeparator(c)) {
break;
}
}
for (; i < mLength; ++i) {
char c = mCharBuf[normalize(mEnd - 1 - i)];
- if (!((LatinIME)mContext).isWordSeparator(c)) {
+ if (!latinIme.isWordSeparator(c)) {
sb.append(c);
} else {
break;
@@ -167,6 +178,7 @@ public final class Utils {
}
return sb.reverse().toString();
}
+
public void reset() {
mLength = 0;
}
@@ -174,11 +186,11 @@ public final class Utils {
// Get the current stack trace
public static String getStackTrace(final int limit) {
- StringBuilder sb = new StringBuilder();
+ final StringBuilder sb = new StringBuilder();
try {
throw new RuntimeException();
- } catch (RuntimeException e) {
- StackTraceElement[] frames = e.getStackTrace();
+ } catch (final RuntimeException e) {
+ final StackTraceElement[] frames = e.getStackTrace();
// Start at 1 because the first frame is here and we don't care about it
for (int j = 1; j < frames.length && j < limit + 1; ++j) {
sb.append(frames[j].toString() + "\n");
@@ -222,7 +234,7 @@ public final class Utils {
return OnDemandInitializationHolder.sInstance;
}
- public void init(InputMethodService ims) {
+ public void init(final InputMethodService ims) {
mIms = ims;
mDirectory = ims.getFilesDir();
}
@@ -232,17 +244,17 @@ public final class Utils {
&& (mDirectory != null && mDirectory.exists())) {
try {
mWriter = getPrintWriter(mDirectory, FILENAME, false);
- } catch (IOException e) {
+ } catch (final IOException e) {
Log.e(USABILITY_TAG, "Can't create log file.");
}
}
}
- public static void writeBackSpace(int x, int y) {
+ public static void writeBackSpace(final int x, final int y) {
UsabilityStudyLogUtils.getInstance().write("<backspace>\t" + x + "\t" + y);
}
- public void writeChar(char c, int x, int y) {
+ public static void writeChar(final char c, final int x, final int y) {
String inputChar = String.valueOf(c);
switch (c) {
case '\n':
@@ -279,15 +291,15 @@ public final class Utils {
private synchronized String getBufferedLogs() {
mWriter.flush();
- StringBuilder sb = new StringBuilder();
- BufferedReader br = getBufferedReader();
+ final StringBuilder sb = new StringBuilder();
+ final BufferedReader br = getBufferedReader();
String line;
try {
while ((line = br.readLine()) != null) {
sb.append('\n');
sb.append(line);
}
- } catch (IOException e) {
+ } catch (final IOException e) {
Log.e(USABILITY_TAG, "Can't read log file.");
} finally {
if (LatinImeLogger.sDBG) {
@@ -295,7 +307,7 @@ public final class Utils {
}
try {
br.close();
- } catch (IOException e) {
+ } catch (final IOException e) {
// ignore.
}
}
@@ -334,10 +346,10 @@ public final class Utils {
srcStream.close();
dest.close();
destStream.close();
- } catch (FileNotFoundException e1) {
+ } catch (final FileNotFoundException e1) {
Log.w(USABILITY_TAG, e1);
return;
- } catch (IOException e2) {
+ } catch (final IOException e2) {
Log.w(USABILITY_TAG, e2);
return;
}
@@ -387,13 +399,13 @@ public final class Utils {
createLogFileIfNotExist();
try {
return new BufferedReader(new FileReader(mFile));
- } catch (FileNotFoundException e) {
+ } catch (final FileNotFoundException e) {
return null;
}
}
- private PrintWriter getPrintWriter(
- File dir, String filename, boolean renew) throws IOException {
+ private PrintWriter getPrintWriter(final File dir, final String filename,
+ final boolean renew) throws IOException {
mFile = new File(dir, filename);
if (mFile.exists()) {
if (renew) {
@@ -405,8 +417,7 @@ public final class Utils {
}
public static final class Stats {
- public static void onNonSeparator(final char code, final int x,
- final int y) {
+ public static void onNonSeparator(final char code, final int x, final int y) {
RingCharBuffer.getInstance().push(code, x, y);
LatinImeLogger.logOnInputChar();
}
@@ -430,7 +441,9 @@ public final class Utils {
public static void onAutoCorrection(final String typedWord, final String correctedWord,
final String separatorString, final WordComposer wordComposer) {
final boolean isBatchMode = wordComposer.isBatchMode();
- if (!isBatchMode && TextUtils.isEmpty(typedWord)) return;
+ if (!isBatchMode && TextUtils.isEmpty(typedWord)) {
+ return;
+ }
// TODO: this fails when the separator is more than 1 code point long, but
// the backend can't handle it yet. The only case when this happens is with
// smileys and other multi-character keys.
@@ -454,36 +467,43 @@ public final class Utils {
}
public static String getDebugInfo(final SuggestedWords suggestions, final int pos) {
- if (!LatinImeLogger.sDBG) return null;
+ if (!LatinImeLogger.sDBG) {
+ return null;
+ }
final SuggestedWordInfo wordInfo = suggestions.getInfo(pos);
- if (wordInfo == null) return null;
+ if (wordInfo == null) {
+ return null;
+ }
final String info = wordInfo.getDebugString();
- if (TextUtils.isEmpty(info)) return null;
+ if (TextUtils.isEmpty(info)) {
+ return null;
+ }
return info;
}
- public static int getAcitivityTitleResId(Context context, Class<? extends Activity> cls) {
+ public static int getAcitivityTitleResId(final Context context,
+ final Class<? extends Activity> cls) {
final ComponentName cn = new ComponentName(context, cls);
try {
final ActivityInfo ai = context.getPackageManager().getActivityInfo(cn, 0);
if (ai != null) {
return ai.labelRes;
}
- } catch (NameNotFoundException e) {
+ } catch (final NameNotFoundException e) {
Log.e(TAG, "Failed to get settings activity title res id.", e);
}
return 0;
}
- public static String getVersionName(Context context) {
+ public static String getVersionName(final Context context) {
try {
if (context == null) {
return "";
}
final String packageName = context.getPackageName();
- PackageInfo info = context.getPackageManager().getPackageInfo(packageName, 0);
+ final PackageInfo info = context.getPackageManager().getPackageInfo(packageName, 0);
return info.versionName;
- } catch (NameNotFoundException e) {
+ } catch (final NameNotFoundException e) {
Log.e(TAG, "Could not find version info.", e);
}
return "";
diff --git a/java/src/com/android/inputmethod/latin/personalization/AccountUtils.java b/java/src/com/android/inputmethod/latin/personalization/AccountUtils.java
index 93687e193..a446672cb 100644
--- a/java/src/com/android/inputmethod/latin/personalization/AccountUtils.java
+++ b/java/src/com/android/inputmethod/latin/personalization/AccountUtils.java
@@ -23,6 +23,7 @@ import android.util.Patterns;
import java.util.ArrayList;
import java.util.List;
+import java.util.Locale;
public class AccountUtils {
private AccountUtils() {
@@ -44,4 +45,22 @@ public class AccountUtils {
}
return retval;
}
+
+ /**
+ * Get all device accounts having specified domain name.
+ * @param context application context
+ * @param domain domain name used for filtering
+ * @return List of account names that contain the specified domain name
+ */
+ public static List<String> getDeviceAccountsWithDomain(
+ final Context context, final String domain) {
+ final ArrayList<String> retval = new ArrayList<String>();
+ final String atDomain = "@" + domain.toLowerCase(Locale.ROOT);
+ for (final Account account : getAccounts(context)) {
+ if (account.name.toLowerCase(Locale.ROOT).endsWith(atDomain)) {
+ retval.add(account.name);
+ }
+ }
+ return retval;
+ }
}
diff --git a/java/src/com/android/inputmethod/latin/setup/LauncherIconVisibilityManager.java b/java/src/com/android/inputmethod/latin/setup/LauncherIconVisibilityManager.java
index 6a7cd9b6f..604ebeeb6 100644
--- a/java/src/com/android/inputmethod/latin/setup/LauncherIconVisibilityManager.java
+++ b/java/src/com/android/inputmethod/latin/setup/LauncherIconVisibilityManager.java
@@ -91,7 +91,7 @@ public final class LauncherIconVisibilityManager extends BroadcastReceiver {
} else if (Intent.ACTION_BOOT_COMPLETED.equals(action)) {
Log.i(TAG, "Boot has been completed");
return true;
- } else if (IntentCompatUtils.has_ACTION_USER_INITIALIZE(intent)) {
+ } else if (IntentCompatUtils.is_ACTION_USER_INITIALIZE(action)) {
Log.i(TAG, "User initialize");
return true;
}
diff --git a/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestions.java b/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestions.java
index 09f81d4c7..322ae5b0f 100644
--- a/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestions.java
+++ b/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestions.java
@@ -61,7 +61,7 @@ public final class MoreSuggestions extends Keyboard {
super();
}
- public int layout(final SuggestedWords suggestedWords, final int fromPos,
+ public int layout(final SuggestedWords suggestedWords, final int fromIndex,
final int maxWidth, final int minWidth, final int maxRow, final Paint paint,
final Resources res) {
clearKeys();
@@ -70,53 +70,54 @@ public final class MoreSuggestions extends Keyboard {
final float padding = res.getDimension(R.dimen.more_suggestions_key_horizontal_padding);
int row = 0;
- int pos = fromPos, rowStartPos = fromPos;
+ int index = fromIndex;
+ int rowStartIndex = fromIndex;
final int size = Math.min(suggestedWords.size(), SuggestionStripView.MAX_SUGGESTIONS);
- while (pos < size) {
- final String word = suggestedWords.getWord(pos);
+ while (index < size) {
+ final String word = suggestedWords.getWord(index);
// TODO: Should take care of text x-scaling.
- mWidths[pos] = (int)(TypefaceUtils.getLabelWidth(word, paint) + padding);
- final int numColumn = pos - rowStartPos + 1;
+ mWidths[index] = (int)(TypefaceUtils.getLabelWidth(word, paint) + padding);
+ final int numColumn = index - rowStartIndex + 1;
final int columnWidth =
(maxWidth - mDividerWidth * (numColumn - 1)) / numColumn;
if (numColumn > MAX_COLUMNS_IN_ROW
- || !fitInWidth(rowStartPos, pos + 1, columnWidth)) {
+ || !fitInWidth(rowStartIndex, index + 1, columnWidth)) {
if ((row + 1) >= maxRow) {
break;
}
- mNumColumnsInRow[row] = pos - rowStartPos;
- rowStartPos = pos;
+ mNumColumnsInRow[row] = index - rowStartIndex;
+ rowStartIndex = index;
row++;
}
- mColumnOrders[pos] = pos - rowStartPos;
- mRowNumbers[pos] = row;
- pos++;
+ mColumnOrders[index] = index - rowStartIndex;
+ mRowNumbers[index] = row;
+ index++;
}
- mNumColumnsInRow[row] = pos - rowStartPos;
+ mNumColumnsInRow[row] = index - rowStartIndex;
mNumRows = row + 1;
mBaseWidth = mOccupiedWidth = Math.max(
- minWidth, calcurateMaxRowWidth(fromPos, pos));
+ minWidth, calcurateMaxRowWidth(fromIndex, index));
mBaseHeight = mOccupiedHeight = mNumRows * mDefaultRowHeight + mVerticalGap;
- return pos - fromPos;
+ return index - fromIndex;
}
- private boolean fitInWidth(final int startPos, final int endPos, final int width) {
- for (int pos = startPos; pos < endPos; pos++) {
- if (mWidths[pos] > width)
+ private boolean fitInWidth(final int startIndex, final int endIndex, final int width) {
+ for (int index = startIndex; index < endIndex; index++) {
+ if (mWidths[index] > width)
return false;
}
return true;
}
- private int calcurateMaxRowWidth(final int startPos, final int endPos) {
+ private int calcurateMaxRowWidth(final int startIndex, final int endIndex) {
int maxRowWidth = 0;
- int pos = startPos;
+ int index = startIndex;
for (int row = 0; row < mNumRows; row++) {
final int numColumnInRow = mNumColumnsInRow[row];
int maxKeyWidth = 0;
- while (pos < endPos && mRowNumbers[pos] == row) {
- maxKeyWidth = Math.max(maxKeyWidth, mWidths[pos]);
- pos++;
+ while (index < endIndex && mRowNumbers[index] == row) {
+ maxKeyWidth = Math.max(maxKeyWidth, mWidths[index]);
+ index++;
}
maxRowWidth = Math.max(maxRowWidth,
maxKeyWidth * numColumnInRow + mDividerWidth * (numColumnInRow - 1));
@@ -130,40 +131,40 @@ public final class MoreSuggestions extends Keyboard {
{ 2, 0, 1},
};
- public int getNumColumnInRow(final int pos) {
- return mNumColumnsInRow[mRowNumbers[pos]];
+ public int getNumColumnInRow(final int index) {
+ return mNumColumnsInRow[mRowNumbers[index]];
}
- public int getColumnNumber(final int pos) {
- final int columnOrder = mColumnOrders[pos];
- final int numColumn = getNumColumnInRow(pos);
+ public int getColumnNumber(final int index) {
+ final int columnOrder = mColumnOrders[index];
+ final int numColumn = getNumColumnInRow(index);
return COLUMN_ORDER_TO_NUMBER[numColumn - 1][columnOrder];
}
- public int getX(final int pos) {
- final int columnNumber = getColumnNumber(pos);
- return columnNumber * (getWidth(pos) + mDividerWidth);
+ public int getX(final int index) {
+ final int columnNumber = getColumnNumber(index);
+ return columnNumber * (getWidth(index) + mDividerWidth);
}
- public int getY(final int pos) {
- final int row = mRowNumbers[pos];
+ public int getY(final int index) {
+ final int row = mRowNumbers[index];
return (mNumRows -1 - row) * mDefaultRowHeight + mTopPadding;
}
- public int getWidth(final int pos) {
- final int numColumnInRow = getNumColumnInRow(pos);
+ public int getWidth(final int index) {
+ final int numColumnInRow = getNumColumnInRow(index);
return (mOccupiedWidth - mDividerWidth * (numColumnInRow - 1)) / numColumnInRow;
}
- public void markAsEdgeKey(final Key key, final int pos) {
- final int row = mRowNumbers[pos];
+ public void markAsEdgeKey(final Key key, final int index) {
+ final int row = mRowNumbers[index];
if (row == 0)
key.markAsBottomEdge(this);
if (row == mNumRows - 1)
key.markAsTopEdge(this);
final int numColumnInRow = mNumColumnsInRow[row];
- final int column = getColumnNumber(pos);
+ final int column = getColumnNumber(index);
if (column == 0)
key.markAsLeftEdge(this);
if (column == numColumnInRow - 1)
@@ -174,15 +175,15 @@ public final class MoreSuggestions extends Keyboard {
public static final class Builder extends KeyboardBuilder<MoreSuggestionsParam> {
private final MoreSuggestionsView mPaneView;
private SuggestedWords mSuggestedWords;
- private int mFromPos;
- private int mToPos;
+ private int mFromIndex;
+ private int mToIndex;
public Builder(final Context context, final MoreSuggestionsView paneView) {
super(context, new MoreSuggestionsParam());
mPaneView = paneView;
}
- public Builder layout(final SuggestedWords suggestedWords, final int fromPos,
+ public Builder layout(final SuggestedWords suggestedWords, final int fromIndex,
final int maxWidth, final int minWidth, final int maxRow,
final Keyboard parentKeyboard) {
final int xmlId = R.xml.kbd_suggestions_pane_template;
@@ -190,10 +191,10 @@ public final class MoreSuggestions extends Keyboard {
mParams.mVerticalGap = mParams.mTopPadding = parentKeyboard.mVerticalGap / 2;
mPaneView.updateKeyboardGeometry(mParams.mDefaultRowHeight);
- final int count = mParams.layout(suggestedWords, fromPos, maxWidth, minWidth, maxRow,
+ final int count = mParams.layout(suggestedWords, fromIndex, maxWidth, minWidth, maxRow,
mPaneView.newLabelPaint(null /* key */), mResources);
- mFromPos = fromPos;
- mToPos = fromPos + count;
+ mFromIndex = fromIndex;
+ mToIndex = fromIndex + count;
mSuggestedWords = suggestedWords;
return this;
}
@@ -201,20 +202,20 @@ public final class MoreSuggestions extends Keyboard {
@Override
public MoreSuggestions build() {
final MoreSuggestionsParam params = mParams;
- for (int pos = mFromPos; pos < mToPos; pos++) {
- final int x = params.getX(pos);
- final int y = params.getY(pos);
- final int width = params.getWidth(pos);
- final String word = mSuggestedWords.getWord(pos);
- final String info = Utils.getDebugInfo(mSuggestedWords, pos);
- final int index = pos + SUGGESTION_CODE_BASE;
+ for (int index = mFromIndex; index < mToIndex; index++) {
+ final int x = params.getX(index);
+ final int y = params.getY(index);
+ final int width = params.getWidth(index);
+ final String word = mSuggestedWords.getWord(index);
+ final String info = Utils.getDebugInfo(mSuggestedWords, index);
+ final int indexInMoreSuggestions = index + SUGGESTION_CODE_BASE;
final Key key = new Key(
- params, word, info, KeyboardIconsSet.ICON_UNDEFINED, index, null, x, y,
- width, params.mDefaultRowHeight, 0);
- params.markAsEdgeKey(key, pos);
+ params, word, info, KeyboardIconsSet.ICON_UNDEFINED, indexInMoreSuggestions,
+ null, x, y, width, params.mDefaultRowHeight, 0);
+ params.markAsEdgeKey(key, index);
params.onAddKey(key);
- final int columnNumber = params.getColumnNumber(pos);
- final int numColumnInRow = params.getNumColumnInRow(pos);
+ final int columnNumber = params.getColumnNumber(index);
+ final int numColumnInRow = params.getNumColumnInRow(index);
if (columnNumber < numColumnInRow - 1) {
final Divider divider = new Divider(params, params.mDivider, x + width, y,
params.mDividerWidth, params.mDefaultRowHeight);
diff --git a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java
new file mode 100644
index 000000000..f434a1211
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java
@@ -0,0 +1,581 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.inputmethod.latin.suggestions;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Paint.Align;
+import android.graphics.Rect;
+import android.graphics.Typeface;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.Spanned;
+import android.text.TextPaint;
+import android.text.TextUtils;
+import android.text.style.CharacterStyle;
+import android.text.style.StyleSpan;
+import android.text.style.UnderlineSpan;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.android.inputmethod.keyboard.ViewLayoutUtils;
+import com.android.inputmethod.latin.AutoCorrection;
+import com.android.inputmethod.latin.LatinImeLogger;
+import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.ResourceUtils;
+import com.android.inputmethod.latin.SuggestedWords;
+import com.android.inputmethod.latin.Utils;
+
+import java.util.ArrayList;
+
+final class SuggestionStripLayoutHelper {
+ private static final int DEFAULT_SUGGESTIONS_COUNT_IN_STRIP = 3;
+ private static final float DEFAULT_CENTER_SUGGESTION_PERCENTILE = 0.40f;
+ private static final int DEFAULT_MAX_MORE_SUGGESTIONS_ROW = 2;
+ private static final int PUNCTUATIONS_IN_STRIP = 5;
+ private static final float MIN_TEXT_XSCALE = 0.70f;
+
+ public final int mPadding;
+ public final int mDividerWidth;
+ public final int mSuggestionsStripHeight;
+ public final int mSuggestionsCountInStrip;
+ public final int mMoreSuggestionsRowHeight;
+ private int mMaxMoreSuggestionsRow;
+ public final float mMinMoreSuggestionsWidth;
+ public final int mMoreSuggestionsBottomGap;
+ public boolean mMoreSuggestionsAvailable;
+
+ // The index of these {@link ArrayList} is the position in the suggestion strip. The indices
+ // increase towards the right for LTR scripts and the left for RTL scripts, starting with 0.
+ // The position of the most important suggestion is in {@link #mCenterPositionInStrip}
+ private final ArrayList<TextView> mWordViews;
+ private final ArrayList<View> mDividerViews;
+ private final ArrayList<TextView> mDebugInfoViews;
+
+ private final int mColorValidTypedWord;
+ private final int mColorTypedWord;
+ private final int mColorAutoCorrect;
+ private final int mColorSuggested;
+ private final float mAlphaObsoleted;
+ private final float mCenterSuggestionWeight;
+ private final int mCenterPositionInStrip;
+ private final Drawable mMoreSuggestionsHint;
+ private static final String MORE_SUGGESTIONS_HINT = "\u2026";
+ private static final String LEFTWARDS_ARROW = "\u2190";
+
+ private static final CharacterStyle BOLD_SPAN = new StyleSpan(Typeface.BOLD);
+ private static final CharacterStyle UNDERLINE_SPAN = new UnderlineSpan();
+
+ private final int mSuggestionStripOption;
+ // These constants are the flag values of
+ // {@link R.styleable#SuggestionStripView_suggestionStripOption} attribute.
+ private static final int AUTO_CORRECT_BOLD = 0x01;
+ private static final int AUTO_CORRECT_UNDERLINE = 0x02;
+ private static final int VALID_TYPED_WORD_BOLD = 0x04;
+
+ private final TextView mWordToSaveView;
+ private final TextView mLeftwardsArrowView;
+ private final TextView mHintToSaveView;
+
+ public SuggestionStripLayoutHelper(final Context context, final AttributeSet attrs,
+ final int defStyle, final ArrayList<TextView> wordViews,
+ final ArrayList<View> dividerViews, final ArrayList<TextView> debugInfoViews) {
+ mWordViews = wordViews;
+ mDividerViews = dividerViews;
+ mDebugInfoViews = debugInfoViews;
+
+ final TextView wordView = wordViews.get(0);
+ final View dividerView = dividerViews.get(0);
+ mPadding = wordView.getCompoundPaddingLeft() + wordView.getCompoundPaddingRight();
+ dividerView.measure(
+ ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
+ mDividerWidth = dividerView.getMeasuredWidth();
+
+ final Resources res = wordView.getResources();
+ mSuggestionsStripHeight = res.getDimensionPixelSize(R.dimen.suggestions_strip_height);
+
+ final TypedArray a = context.obtainStyledAttributes(attrs,
+ R.styleable.SuggestionStripView, defStyle, R.style.SuggestionStripViewStyle);
+ mSuggestionStripOption = a.getInt(
+ R.styleable.SuggestionStripView_suggestionStripOption, 0);
+ final float alphaValidTypedWord = ResourceUtils.getFraction(a,
+ R.styleable.SuggestionStripView_alphaValidTypedWord, 1.0f);
+ final float alphaTypedWord = ResourceUtils.getFraction(a,
+ R.styleable.SuggestionStripView_alphaTypedWord, 1.0f);
+ final float alphaAutoCorrect = ResourceUtils.getFraction(a,
+ R.styleable.SuggestionStripView_alphaAutoCorrect, 1.0f);
+ final float alphaSuggested = ResourceUtils.getFraction(a,
+ R.styleable.SuggestionStripView_alphaSuggested, 1.0f);
+ mAlphaObsoleted = ResourceUtils.getFraction(a,
+ R.styleable.SuggestionStripView_alphaSuggested, 1.0f);
+ mColorValidTypedWord = applyAlpha(a.getColor(
+ R.styleable.SuggestionStripView_colorValidTypedWord, 0), alphaValidTypedWord);
+ mColorTypedWord = applyAlpha(a.getColor(
+ R.styleable.SuggestionStripView_colorTypedWord, 0), alphaTypedWord);
+ mColorAutoCorrect = applyAlpha(a.getColor(
+ R.styleable.SuggestionStripView_colorAutoCorrect, 0), alphaAutoCorrect);
+ mColorSuggested = applyAlpha(a.getColor(
+ R.styleable.SuggestionStripView_colorSuggested, 0), alphaSuggested);
+ mSuggestionsCountInStrip = a.getInt(
+ R.styleable.SuggestionStripView_suggestionsCountInStrip,
+ DEFAULT_SUGGESTIONS_COUNT_IN_STRIP);
+ mCenterSuggestionWeight = ResourceUtils.getFraction(a,
+ R.styleable.SuggestionStripView_centerSuggestionPercentile,
+ DEFAULT_CENTER_SUGGESTION_PERCENTILE);
+ mMaxMoreSuggestionsRow = a.getInt(
+ R.styleable.SuggestionStripView_maxMoreSuggestionsRow,
+ DEFAULT_MAX_MORE_SUGGESTIONS_ROW);
+ mMinMoreSuggestionsWidth = ResourceUtils.getFraction(a,
+ R.styleable.SuggestionStripView_minMoreSuggestionsWidth, 1.0f);
+ a.recycle();
+
+ mMoreSuggestionsHint = getMoreSuggestionsHint(res,
+ res.getDimension(R.dimen.more_suggestions_hint_text_size), mColorAutoCorrect);
+ mCenterPositionInStrip = mSuggestionsCountInStrip / 2;
+ mMoreSuggestionsBottomGap = res.getDimensionPixelOffset(
+ R.dimen.more_suggestions_bottom_gap);
+ mMoreSuggestionsRowHeight = res.getDimensionPixelSize(R.dimen.more_suggestions_row_height);
+
+ final LayoutInflater inflater = LayoutInflater.from(context);
+ mWordToSaveView = (TextView)inflater.inflate(R.layout.suggestion_word, null);
+ mLeftwardsArrowView = (TextView)inflater.inflate(R.layout.hint_add_to_dictionary, null);
+ mHintToSaveView = (TextView)inflater.inflate(R.layout.hint_add_to_dictionary, null);
+ }
+
+ public int getMaxMoreSuggestionsRow() {
+ return mMaxMoreSuggestionsRow;
+ }
+
+ private int getMoreSuggestionsHeight() {
+ return mMaxMoreSuggestionsRow * mMoreSuggestionsRowHeight + mMoreSuggestionsBottomGap;
+ }
+
+ public int setMoreSuggestionsHeight(final int remainingHeight) {
+ final int currentHeight = getMoreSuggestionsHeight();
+ if (currentHeight <= remainingHeight) {
+ return currentHeight;
+ }
+
+ mMaxMoreSuggestionsRow = (remainingHeight - mMoreSuggestionsBottomGap)
+ / mMoreSuggestionsRowHeight;
+ final int newHeight = getMoreSuggestionsHeight();
+ return newHeight;
+ }
+
+ private static Drawable getMoreSuggestionsHint(final Resources res, final float textSize,
+ final int color) {
+ final Paint paint = new Paint();
+ paint.setAntiAlias(true);
+ paint.setTextAlign(Align.CENTER);
+ paint.setTextSize(textSize);
+ paint.setColor(color);
+ final Rect bounds = new Rect();
+ paint.getTextBounds(MORE_SUGGESTIONS_HINT, 0, MORE_SUGGESTIONS_HINT.length(), bounds);
+ final int width = Math.round(bounds.width() + 0.5f);
+ final int height = Math.round(bounds.height() + 0.5f);
+ final Bitmap buffer = Bitmap.createBitmap(width, (height * 3 / 2), Bitmap.Config.ARGB_8888);
+ final Canvas canvas = new Canvas(buffer);
+ canvas.drawText(MORE_SUGGESTIONS_HINT, width / 2, height, paint);
+ return new BitmapDrawable(res, buffer);
+ }
+
+ private CharSequence getStyledSuggestedWord(final SuggestedWords suggestedWords,
+ final int indexInSuggestedWords) {
+ if (indexInSuggestedWords >= suggestedWords.size()) {
+ return null;
+ }
+ final String word = suggestedWords.getWord(indexInSuggestedWords);
+ final boolean isAutoCorrect = indexInSuggestedWords == 1
+ && suggestedWords.willAutoCorrect();
+ final boolean isTypedWordValid = indexInSuggestedWords == 0
+ && suggestedWords.mTypedWordValid;
+ if (!isAutoCorrect && !isTypedWordValid) {
+ return word;
+ }
+
+ final int len = word.length();
+ final Spannable spannedWord = new SpannableString(word);
+ final int option = mSuggestionStripOption;
+ if ((isAutoCorrect && (option & AUTO_CORRECT_BOLD) != 0)
+ || (isTypedWordValid && (option & VALID_TYPED_WORD_BOLD) != 0)) {
+ spannedWord.setSpan(BOLD_SPAN, 0, len, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
+ }
+ if (isAutoCorrect && (option & AUTO_CORRECT_UNDERLINE) != 0) {
+ spannedWord.setSpan(UNDERLINE_SPAN, 0, len, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
+ }
+ return spannedWord;
+ }
+
+ private int getIndexInSuggestedWords(final int positionInStrip,
+ final SuggestedWords suggestedWords) {
+ // TODO: This works for 3 suggestions. Revisit this algorithm when there are 5 or more
+ // suggestions.
+ final int mostImportantIndexInSuggestedWords = suggestedWords.willAutoCorrect()
+ ? SuggestedWords.INDEX_OF_AUTO_CORRECTION : SuggestedWords.INDEX_OF_TYPED_WORD;
+ if (positionInStrip == mCenterPositionInStrip) {
+ return mostImportantIndexInSuggestedWords;
+ }
+ if (positionInStrip == mostImportantIndexInSuggestedWords) {
+ return mCenterPositionInStrip;
+ }
+ return positionInStrip;
+ }
+
+ private int getSuggestionTextColor(final int positionInStrip,
+ final SuggestedWords suggestedWords) {
+ final int indexInSuggestedWords = getIndexInSuggestedWords(positionInStrip, suggestedWords);
+ // TODO: Need to revisit this logic with bigram suggestions
+ final boolean isSuggested = (indexInSuggestedWords != SuggestedWords.INDEX_OF_TYPED_WORD);
+
+ final int color;
+ if (positionInStrip == mCenterPositionInStrip && suggestedWords.willAutoCorrect()) {
+ color = mColorAutoCorrect;
+ } else if (positionInStrip == mCenterPositionInStrip && suggestedWords.mTypedWordValid) {
+ color = mColorValidTypedWord;
+ } else if (isSuggested) {
+ color = mColorSuggested;
+ } else {
+ color = mColorTypedWord;
+ }
+ if (LatinImeLogger.sDBG && suggestedWords.size() > 1) {
+ // If we auto-correct, then the autocorrection is in slot 0 and the typed word
+ // is in slot 1.
+ if (positionInStrip == mCenterPositionInStrip
+ && AutoCorrection.shouldBlockAutoCorrectionBySafetyNet(
+ suggestedWords.getWord(SuggestedWords.INDEX_OF_AUTO_CORRECTION),
+ suggestedWords.getWord(SuggestedWords.INDEX_OF_TYPED_WORD))) {
+ return 0xFFFF0000;
+ }
+ }
+
+ if (suggestedWords.mIsObsoleteSuggestions && isSuggested) {
+ return applyAlpha(color, mAlphaObsoleted);
+ }
+ return color;
+ }
+
+ private static int applyAlpha(final int color, final float alpha) {
+ final int newAlpha = (int)(Color.alpha(color) * alpha);
+ return Color.argb(newAlpha, Color.red(color), Color.green(color), Color.blue(color));
+ }
+
+ private static void addDivider(final ViewGroup stripView, final View dividerView) {
+ stripView.addView(dividerView);
+ final LinearLayout.LayoutParams params =
+ (LinearLayout.LayoutParams)dividerView.getLayoutParams();
+ params.gravity = Gravity.CENTER;
+ }
+
+ public void layout(final SuggestedWords suggestedWords, final ViewGroup stripView,
+ final ViewGroup placerView) {
+ if (suggestedWords.mIsPunctuationSuggestions) {
+ layoutPunctuationSuggestions(suggestedWords, stripView);
+ return;
+ }
+
+ final int countInStrip = mSuggestionsCountInStrip;
+ setupWordViewsTextAndColor(suggestedWords, countInStrip);
+ final TextView centerWordView = mWordViews.get(mCenterPositionInStrip);
+ final int stripWidth = placerView.getWidth();
+ final int centerWidth = getSuggestionWidth(mCenterPositionInStrip, stripWidth);
+ if (getTextScaleX(centerWordView.getText(), centerWidth, centerWordView.getPaint())
+ < MIN_TEXT_XSCALE) {
+ // Layout only the most relevant suggested word at the center of the suggestion strip
+ // by consolidating all slots in the strip.
+ mMoreSuggestionsAvailable = (suggestedWords.size() > 1);
+ layoutWord(mCenterPositionInStrip, stripWidth);
+ stripView.addView(centerWordView);
+ setLayoutWeight(centerWordView, 1.0f, ViewGroup.LayoutParams.MATCH_PARENT);
+ if (SuggestionStripView.DBG) {
+ layoutDebugInfo(mCenterPositionInStrip, placerView, stripWidth);
+ }
+ return;
+ }
+
+ mMoreSuggestionsAvailable = (suggestedWords.size() > countInStrip);
+ int x = 0;
+ for (int positionInStrip = 0; positionInStrip < countInStrip; positionInStrip++) {
+ if (positionInStrip != 0) {
+ final View divider = mDividerViews.get(positionInStrip);
+ // Add divider if this isn't the left most suggestion in suggestions strip.
+ addDivider(stripView, divider);
+ x += divider.getMeasuredWidth();
+ }
+
+ final int width = getSuggestionWidth(positionInStrip, stripWidth);
+ final TextView wordView = layoutWord(positionInStrip, width);
+ stripView.addView(wordView);
+ setLayoutWeight(wordView, getSuggestionWeight(positionInStrip),
+ ViewGroup.LayoutParams.MATCH_PARENT);
+ x += wordView.getMeasuredWidth();
+
+ if (SuggestionStripView.DBG) {
+ layoutDebugInfo(positionInStrip, placerView, x);
+ }
+ }
+ }
+
+ /**
+ * Format appropriately the suggested word in {@link #mWordViews} specified by
+ * <code>positionInStrip</code>. When the suggested word doesn't exist, the corresponding
+ * {@link TextView} will be disabled and never respond to user interaction. The suggested word
+ * may be shrunk or ellipsized to fit in the specified width.
+ *
+ * The <code>positionInStrip</code> argument is the index in the suggestion strip. The indices
+ * increase towards the right for LTR scripts and the left for RTL scripts, starting with 0.
+ * The position of the most important suggestion is in {@link #mCenterPositionInStrip}. This
+ * usually doesn't match the index in <code>suggedtedWords</code> -- see
+ * {@link #getIndexInSuggestedWords(int,SuggestedWords)}.
+ *
+ * @param positionInStrip the position in the suggestion strip.
+ * @param width the maximum width for layout in pixels.
+ * @return the {@link TextView} containing the suggested word appropriately formatted.
+ */
+ private TextView layoutWord(final int positionInStrip, final int width) {
+ final TextView wordView = mWordViews.get(positionInStrip);
+ final CharSequence word = wordView.getText();
+ if (positionInStrip == mCenterPositionInStrip && mMoreSuggestionsAvailable) {
+ // TODO: This "more suggestions hint" should have a nicely designed icon.
+ wordView.setCompoundDrawablesWithIntrinsicBounds(
+ null, null, null, mMoreSuggestionsHint);
+ // HACK: Align with other TextViews that have no compound drawables.
+ wordView.setCompoundDrawablePadding(-mMoreSuggestionsHint.getIntrinsicHeight());
+ } else {
+ wordView.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null);
+ }
+
+ // Disable this suggestion if the suggestion is null or empty.
+ wordView.setEnabled(!TextUtils.isEmpty(word));
+ final CharSequence text = getEllipsizedText(word, width, wordView.getPaint());
+ final float scaleX = wordView.getTextScaleX();
+ wordView.setText(text); // TextView.setText() resets text scale x to 1.0.
+ wordView.setTextScaleX(scaleX);
+ return wordView;
+ }
+
+ private void layoutDebugInfo(final int positionInStrip, final ViewGroup placerView,
+ final int x) {
+ final TextView debugInfoView = mDebugInfoViews.get(positionInStrip);
+ final CharSequence debugInfo = debugInfoView.getText();
+ if (debugInfo == null) {
+ return;
+ }
+ placerView.addView(debugInfoView);
+ debugInfoView.measure(
+ ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
+ final int infoWidth = debugInfoView.getMeasuredWidth();
+ final int y = debugInfoView.getMeasuredHeight();
+ ViewLayoutUtils.placeViewAt(
+ debugInfoView, x - infoWidth, y, infoWidth, debugInfoView.getMeasuredHeight());
+ }
+
+ private int getSuggestionWidth(final int positionInStrip, final int maxWidth) {
+ final int paddings = mPadding * mSuggestionsCountInStrip;
+ final int dividers = mDividerWidth * (mSuggestionsCountInStrip - 1);
+ final int availableWidth = maxWidth - paddings - dividers;
+ return (int)(availableWidth * getSuggestionWeight(positionInStrip));
+ }
+
+ private float getSuggestionWeight(final int positionInStrip) {
+ if (positionInStrip == mCenterPositionInStrip) {
+ return mCenterSuggestionWeight;
+ }
+ // TODO: Revisit this for cases of 5 or more suggestions
+ return (1.0f - mCenterSuggestionWeight) / (mSuggestionsCountInStrip - 1);
+ }
+
+ private void setupWordViewsTextAndColor(final SuggestedWords suggestedWords,
+ final int countInStrip) {
+ final int count = Math.min(suggestedWords.size(), countInStrip);
+ for (int positionInStrip = 0; positionInStrip < count; positionInStrip++) {
+ final int indexInSuggestedWords =
+ getIndexInSuggestedWords(positionInStrip, suggestedWords);
+ final TextView wordView = mWordViews.get(positionInStrip);
+ // {@link TextView#getTag()} is used to get the index in suggestedWords at
+ // {@link SuggestionStripView#onClick(View)}.
+ wordView.setTag(indexInSuggestedWords);
+ wordView.setText(getStyledSuggestedWord(suggestedWords, indexInSuggestedWords));
+ wordView.setTextColor(getSuggestionTextColor(positionInStrip, suggestedWords));
+ if (SuggestionStripView.DBG) {
+ mDebugInfoViews.get(positionInStrip).setText(
+ Utils.getDebugInfo(suggestedWords, indexInSuggestedWords));
+ }
+ }
+ for (int positionInStrip = count; positionInStrip < countInStrip; positionInStrip++) {
+ mWordViews.get(positionInStrip).setText(null);
+ // Make this inactive for touches in {@link #layoutWord(int,int)}.
+ if (SuggestionStripView.DBG) {
+ mDebugInfoViews.get(positionInStrip).setText(null);
+ }
+ }
+ }
+
+ private void layoutPunctuationSuggestions(final SuggestedWords suggestedWords,
+ final ViewGroup stripView) {
+ final int countInStrip = Math.min(suggestedWords.size(), PUNCTUATIONS_IN_STRIP);
+ for (int positionInStrip = 0; positionInStrip < countInStrip; positionInStrip++) {
+ if (positionInStrip != 0) {
+ // Add divider if this isn't the left most suggestion in suggestions strip.
+ addDivider(stripView, mDividerViews.get(positionInStrip));
+ }
+
+ final TextView wordView = mWordViews.get(positionInStrip);
+ wordView.setEnabled(true);
+ wordView.setTextColor(mColorAutoCorrect);
+ final String punctuation = suggestedWords.getWord(positionInStrip);
+ wordView.setText(punctuation);
+ wordView.setTextScaleX(1.0f);
+ wordView.setCompoundDrawables(null, null, null, null);
+ stripView.addView(wordView);
+ setLayoutWeight(wordView, 1.0f, mSuggestionsStripHeight);
+ }
+ mMoreSuggestionsAvailable = (suggestedWords.size() > countInStrip);
+ }
+
+ public void layoutAddToDictionaryHint(final String word, final ViewGroup stripView,
+ final int stripWidth, final CharSequence hintText, final OnClickListener listener) {
+ final int width = stripWidth - mDividerWidth - mPadding * 2;
+
+ final TextView wordView = mWordToSaveView;
+ wordView.setTextColor(mColorTypedWord);
+ final int wordWidth = (int)(width * mCenterSuggestionWeight);
+ final CharSequence text = getEllipsizedText(word, wordWidth, wordView.getPaint());
+ final float wordScaleX = wordView.getTextScaleX();
+ wordView.setTag(word);
+ wordView.setText(text);
+ wordView.setTextScaleX(wordScaleX);
+ stripView.addView(wordView);
+ setLayoutWeight(wordView, mCenterSuggestionWeight, ViewGroup.LayoutParams.MATCH_PARENT);
+
+ stripView.addView(mDividerViews.get(0));
+
+ final TextView leftArrowView = mLeftwardsArrowView;
+ leftArrowView.setTextColor(mColorAutoCorrect);
+ leftArrowView.setText(LEFTWARDS_ARROW);
+ stripView.addView(leftArrowView);
+
+ final TextView hintView = mHintToSaveView;
+ hintView.setGravity(Gravity.LEFT | Gravity.CENTER_VERTICAL);
+ hintView.setTextColor(mColorAutoCorrect);
+ final int hintWidth = width - wordWidth - leftArrowView.getWidth();
+ final float hintScaleX = getTextScaleX(hintText, hintWidth, hintView.getPaint());
+ hintView.setText(hintText);
+ hintView.setTextScaleX(hintScaleX);
+ stripView.addView(hintView);
+ setLayoutWeight(
+ hintView, 1.0f - mCenterSuggestionWeight, ViewGroup.LayoutParams.MATCH_PARENT);
+
+ wordView.setOnClickListener(listener);
+ leftArrowView.setOnClickListener(listener);
+ hintView.setOnClickListener(listener);
+ }
+
+ public CharSequence getAddToDictionaryWord() {
+ return (CharSequence)mWordToSaveView.getTag();
+ }
+
+ public boolean isAddToDictionaryShowing(final View v) {
+ return v == mWordToSaveView || v == mHintToSaveView || v == mLeftwardsArrowView;
+ }
+
+ private static void setLayoutWeight(final View v, final float weight, final int height) {
+ final ViewGroup.LayoutParams lp = v.getLayoutParams();
+ if (lp instanceof LinearLayout.LayoutParams) {
+ final LinearLayout.LayoutParams llp = (LinearLayout.LayoutParams)lp;
+ llp.weight = weight;
+ llp.width = 0;
+ llp.height = height;
+ }
+ }
+
+ private static float getTextScaleX(final CharSequence text, final int maxWidth,
+ final TextPaint paint) {
+ paint.setTextScaleX(1.0f);
+ final int width = getTextWidth(text, paint);
+ if (width <= maxWidth) {
+ return 1.0f;
+ }
+ return maxWidth / (float)width;
+ }
+
+ private static CharSequence getEllipsizedText(final CharSequence text, final int maxWidth,
+ final TextPaint paint) {
+ if (text == null) {
+ return null;
+ }
+ final float scaleX = getTextScaleX(text, maxWidth, paint);
+ if (scaleX >= MIN_TEXT_XSCALE) {
+ paint.setTextScaleX(scaleX);
+ return text;
+ }
+
+ // Note that TextUtils.ellipsize() use text-x-scale as 1.0 if ellipsize is needed. To
+ // get squeezed and ellipsized text, passes enlarged width (maxWidth / MIN_TEXT_XSCALE).
+ final CharSequence ellipsized = TextUtils.ellipsize(
+ text, paint, maxWidth / MIN_TEXT_XSCALE, TextUtils.TruncateAt.MIDDLE);
+ paint.setTextScaleX(MIN_TEXT_XSCALE);
+ return ellipsized;
+ }
+
+ private static int getTextWidth(final CharSequence text, final TextPaint paint) {
+ if (TextUtils.isEmpty(text)) {
+ return 0;
+ }
+ final Typeface savedTypeface = paint.getTypeface();
+ paint.setTypeface(getTextTypeface(text));
+ final int len = text.length();
+ final float[] widths = new float[len];
+ final int count = paint.getTextWidths(text, 0, len, widths);
+ int width = 0;
+ for (int i = 0; i < count; i++) {
+ width += Math.round(widths[i] + 0.5f);
+ }
+ paint.setTypeface(savedTypeface);
+ return width;
+ }
+
+ private static Typeface getTextTypeface(final CharSequence text) {
+ if (!(text instanceof SpannableString)) {
+ return Typeface.DEFAULT;
+ }
+
+ final SpannableString ss = (SpannableString)text;
+ final StyleSpan[] styles = ss.getSpans(0, text.length(), StyleSpan.class);
+ if (styles.length == 0) {
+ return Typeface.DEFAULT;
+ }
+
+ if (styles[0].getStyle() == Typeface.BOLD) {
+ return Typeface.DEFAULT_BOLD;
+ }
+ // TODO: BOLD_ITALIC, ITALIC case?
+ return Typeface.DEFAULT;
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java
index ad350a02f..b2b9427af 100644
--- a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java
+++ b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java
@@ -18,34 +18,14 @@ package com.android.inputmethod.latin.suggestions;
import android.content.Context;
import android.content.res.Resources;
-import android.content.res.TypedArray;
-import android.graphics.Bitmap;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.Paint;
-import android.graphics.Paint.Align;
-import android.graphics.Rect;
-import android.graphics.Typeface;
-import android.graphics.drawable.BitmapDrawable;
-import android.graphics.drawable.Drawable;
-import android.text.Spannable;
-import android.text.SpannableString;
-import android.text.Spanned;
-import android.text.TextPaint;
-import android.text.TextUtils;
-import android.text.style.CharacterStyle;
-import android.text.style.StyleSpan;
-import android.text.style.UnderlineSpan;
import android.util.AttributeSet;
import android.view.GestureDetector;
-import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnLongClickListener;
import android.view.ViewGroup;
-import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import android.widget.TextView;
@@ -53,16 +33,13 @@ import com.android.inputmethod.keyboard.Keyboard;
import com.android.inputmethod.keyboard.KeyboardSwitcher;
import com.android.inputmethod.keyboard.MainKeyboardView;
import com.android.inputmethod.keyboard.MoreKeysPanel;
-import com.android.inputmethod.keyboard.ViewLayoutUtils;
-import com.android.inputmethod.latin.AutoCorrection;
+import com.android.inputmethod.latin.AudioAndHapticFeedbackManager;
import com.android.inputmethod.latin.CollectionUtils;
import com.android.inputmethod.latin.Constants;
import com.android.inputmethod.latin.LatinImeLogger;
import com.android.inputmethod.latin.R;
-import com.android.inputmethod.latin.ResourceUtils;
import com.android.inputmethod.latin.SuggestedWords;
import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
-import com.android.inputmethod.latin.Utils;
import com.android.inputmethod.latin.define.ProductionFlag;
import com.android.inputmethod.latin.suggestions.MoreSuggestions.MoreSuggestionsListener;
import com.android.inputmethod.research.ResearchLogger;
@@ -88,477 +65,14 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick
private final MoreSuggestionsView mMoreSuggestionsView;
private final MoreSuggestions.Builder mMoreSuggestionsBuilder;
- private final ArrayList<TextView> mWords = CollectionUtils.newArrayList();
- private final ArrayList<TextView> mInfos = CollectionUtils.newArrayList();
- private final ArrayList<View> mDividers = CollectionUtils.newArrayList();
+ private final ArrayList<TextView> mWordViews = CollectionUtils.newArrayList();
+ private final ArrayList<TextView> mDebugInfoViews = CollectionUtils.newArrayList();
+ private final ArrayList<View> mDividerViews = CollectionUtils.newArrayList();
Listener mListener;
private SuggestedWords mSuggestedWords = SuggestedWords.EMPTY;
- private final SuggestionStripViewParams mParams;
- private static final float MIN_TEXT_XSCALE = 0.70f;
-
- private static final class SuggestionStripViewParams {
- private static final int DEFAULT_SUGGESTIONS_COUNT_IN_STRIP = 3;
- private static final float DEFAULT_CENTER_SUGGESTION_PERCENTILE = 0.40f;
- private static final int DEFAULT_MAX_MORE_SUGGESTIONS_ROW = 2;
- private static final int PUNCTUATIONS_IN_STRIP = 5;
-
- public final int mPadding;
- public final int mDividerWidth;
- public final int mSuggestionsStripHeight;
- public final int mSuggestionsCountInStrip;
- public final int mMoreSuggestionsRowHeight;
- private int mMaxMoreSuggestionsRow;
- public final float mMinMoreSuggestionsWidth;
- public final int mMoreSuggestionsBottomGap;
-
- private final ArrayList<TextView> mWords;
- private final ArrayList<View> mDividers;
- private final ArrayList<TextView> mInfos;
-
- private final int mColorValidTypedWord;
- private final int mColorTypedWord;
- private final int mColorAutoCorrect;
- private final int mColorSuggested;
- private final float mAlphaObsoleted;
- private final float mCenterSuggestionWeight;
- private final int mCenterSuggestionIndex;
- private final Drawable mMoreSuggestionsHint;
- private static final String MORE_SUGGESTIONS_HINT = "\u2026";
- private static final String LEFTWARDS_ARROW = "\u2190";
-
- private static final CharacterStyle BOLD_SPAN = new StyleSpan(Typeface.BOLD);
- private static final CharacterStyle UNDERLINE_SPAN = new UnderlineSpan();
- private static final int AUTO_CORRECT_BOLD = 0x01;
- private static final int AUTO_CORRECT_UNDERLINE = 0x02;
- private static final int VALID_TYPED_WORD_BOLD = 0x04;
-
- private final int mSuggestionStripOption;
-
- private final ArrayList<CharSequence> mTexts = CollectionUtils.newArrayList();
-
- public boolean mMoreSuggestionsAvailable;
-
- private final TextView mWordToSaveView;
- private final TextView mLeftwardsArrowView;
- private final TextView mHintToSaveView;
-
- public SuggestionStripViewParams(final Context context, final AttributeSet attrs,
- final int defStyle, final ArrayList<TextView> words, final ArrayList<View> dividers,
- final ArrayList<TextView> infos) {
- mWords = words;
- mDividers = dividers;
- mInfos = infos;
-
- final TextView word = words.get(0);
- final View divider = dividers.get(0);
- mPadding = word.getCompoundPaddingLeft() + word.getCompoundPaddingRight();
- divider.measure(
- ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
- mDividerWidth = divider.getMeasuredWidth();
-
- final Resources res = word.getResources();
- mSuggestionsStripHeight = res.getDimensionPixelSize(R.dimen.suggestions_strip_height);
-
- final TypedArray a = context.obtainStyledAttributes(attrs,
- R.styleable.SuggestionStripView, defStyle, R.style.SuggestionStripViewStyle);
- mSuggestionStripOption = a.getInt(
- R.styleable.SuggestionStripView_suggestionStripOption, 0);
- final float alphaValidTypedWord = ResourceUtils.getFraction(a,
- R.styleable.SuggestionStripView_alphaValidTypedWord, 1.0f);
- final float alphaTypedWord = ResourceUtils.getFraction(a,
- R.styleable.SuggestionStripView_alphaTypedWord, 1.0f);
- final float alphaAutoCorrect = ResourceUtils.getFraction(a,
- R.styleable.SuggestionStripView_alphaAutoCorrect, 1.0f);
- final float alphaSuggested = ResourceUtils.getFraction(a,
- R.styleable.SuggestionStripView_alphaSuggested, 1.0f);
- mAlphaObsoleted = ResourceUtils.getFraction(a,
- R.styleable.SuggestionStripView_alphaSuggested, 1.0f);
- mColorValidTypedWord = applyAlpha(a.getColor(
- R.styleable.SuggestionStripView_colorValidTypedWord, 0), alphaValidTypedWord);
- mColorTypedWord = applyAlpha(a.getColor(
- R.styleable.SuggestionStripView_colorTypedWord, 0), alphaTypedWord);
- mColorAutoCorrect = applyAlpha(a.getColor(
- R.styleable.SuggestionStripView_colorAutoCorrect, 0), alphaAutoCorrect);
- mColorSuggested = applyAlpha(a.getColor(
- R.styleable.SuggestionStripView_colorSuggested, 0), alphaSuggested);
- mSuggestionsCountInStrip = a.getInt(
- R.styleable.SuggestionStripView_suggestionsCountInStrip,
- DEFAULT_SUGGESTIONS_COUNT_IN_STRIP);
- mCenterSuggestionWeight = ResourceUtils.getFraction(a,
- R.styleable.SuggestionStripView_centerSuggestionPercentile,
- DEFAULT_CENTER_SUGGESTION_PERCENTILE);
- mMaxMoreSuggestionsRow = a.getInt(
- R.styleable.SuggestionStripView_maxMoreSuggestionsRow,
- DEFAULT_MAX_MORE_SUGGESTIONS_ROW);
- mMinMoreSuggestionsWidth = ResourceUtils.getFraction(a,
- R.styleable.SuggestionStripView_minMoreSuggestionsWidth, 1.0f);
- a.recycle();
-
- mMoreSuggestionsHint = getMoreSuggestionsHint(res,
- res.getDimension(R.dimen.more_suggestions_hint_text_size), mColorAutoCorrect);
- mCenterSuggestionIndex = mSuggestionsCountInStrip / 2;
- mMoreSuggestionsBottomGap = res.getDimensionPixelOffset(
- R.dimen.more_suggestions_bottom_gap);
- mMoreSuggestionsRowHeight = res.getDimensionPixelSize(
- R.dimen.more_suggestions_row_height);
-
- final LayoutInflater inflater = LayoutInflater.from(context);
- mWordToSaveView = (TextView)inflater.inflate(R.layout.suggestion_word, null);
- mLeftwardsArrowView = (TextView)inflater.inflate(R.layout.hint_add_to_dictionary, null);
- mHintToSaveView = (TextView)inflater.inflate(R.layout.hint_add_to_dictionary, null);
- }
-
- public int getMaxMoreSuggestionsRow() {
- return mMaxMoreSuggestionsRow;
- }
-
- private int getMoreSuggestionsHeight() {
- return mMaxMoreSuggestionsRow * mMoreSuggestionsRowHeight + mMoreSuggestionsBottomGap;
- }
-
- public int setMoreSuggestionsHeight(final int remainingHeight) {
- final int currentHeight = getMoreSuggestionsHeight();
- if (currentHeight <= remainingHeight) {
- return currentHeight;
- }
-
- mMaxMoreSuggestionsRow = (remainingHeight - mMoreSuggestionsBottomGap)
- / mMoreSuggestionsRowHeight;
- final int newHeight = getMoreSuggestionsHeight();
- return newHeight;
- }
-
- private static Drawable getMoreSuggestionsHint(final Resources res, final float textSize,
- final int color) {
- final Paint paint = new Paint();
- paint.setAntiAlias(true);
- paint.setTextAlign(Align.CENTER);
- paint.setTextSize(textSize);
- paint.setColor(color);
- final Rect bounds = new Rect();
- paint.getTextBounds(MORE_SUGGESTIONS_HINT, 0, MORE_SUGGESTIONS_HINT.length(), bounds);
- final int width = Math.round(bounds.width() + 0.5f);
- final int height = Math.round(bounds.height() + 0.5f);
- final Bitmap buffer = Bitmap.createBitmap(
- width, (height * 3 / 2), Bitmap.Config.ARGB_8888);
- final Canvas canvas = new Canvas(buffer);
- canvas.drawText(MORE_SUGGESTIONS_HINT, width / 2, height, paint);
- return new BitmapDrawable(res, buffer);
- }
-
- private CharSequence getStyledSuggestionWord(final SuggestedWords suggestedWords,
- final int pos) {
- final String word = suggestedWords.getWord(pos);
- final boolean isAutoCorrect = pos == 1 && suggestedWords.willAutoCorrect();
- final boolean isTypedWordValid = pos == 0 && suggestedWords.mTypedWordValid;
- if (!isAutoCorrect && !isTypedWordValid)
- return word;
-
- final int len = word.length();
- final Spannable spannedWord = new SpannableString(word);
- final int option = mSuggestionStripOption;
- if ((isAutoCorrect && (option & AUTO_CORRECT_BOLD) != 0)
- || (isTypedWordValid && (option & VALID_TYPED_WORD_BOLD) != 0)) {
- spannedWord.setSpan(BOLD_SPAN, 0, len, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
- }
- if (isAutoCorrect && (option & AUTO_CORRECT_UNDERLINE) != 0) {
- spannedWord.setSpan(UNDERLINE_SPAN, 0, len, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
- }
- return spannedWord;
- }
-
- private int getWordPosition(final int index, final SuggestedWords suggestedWords) {
- // TODO: This works for 3 suggestions. Revisit this algorithm when there are 5 or more
- // suggestions.
- final int centerPos = suggestedWords.willAutoCorrect() ? 1 : 0;
- if (index == mCenterSuggestionIndex) {
- return centerPos;
- } else if (index == centerPos) {
- return mCenterSuggestionIndex;
- } else {
- return index;
- }
- }
-
- private int getSuggestionTextColor(final int index, final SuggestedWords suggestedWords,
- final int pos) {
- // TODO: Need to revisit this logic with bigram suggestions
- final boolean isSuggested = (pos != 0);
-
- final int color;
- if (index == mCenterSuggestionIndex && suggestedWords.willAutoCorrect()) {
- color = mColorAutoCorrect;
- } else if (index == mCenterSuggestionIndex && suggestedWords.mTypedWordValid) {
- color = mColorValidTypedWord;
- } else if (isSuggested) {
- color = mColorSuggested;
- } else {
- color = mColorTypedWord;
- }
- if (LatinImeLogger.sDBG && suggestedWords.size() > 1) {
- // If we auto-correct, then the autocorrection is in slot 0 and the typed word
- // is in slot 1.
- if (index == mCenterSuggestionIndex
- && AutoCorrection.shouldBlockAutoCorrectionBySafetyNet(
- suggestedWords.getWord(1), suggestedWords.getWord(0))) {
- return 0xFFFF0000;
- }
- }
-
- if (suggestedWords.mIsObsoleteSuggestions && isSuggested) {
- return applyAlpha(color, mAlphaObsoleted);
- } else {
- return color;
- }
- }
-
- private static int applyAlpha(final int color, final float alpha) {
- final int newAlpha = (int)(Color.alpha(color) * alpha);
- return Color.argb(newAlpha, Color.red(color), Color.green(color), Color.blue(color));
- }
-
- private static void addDivider(final ViewGroup stripView, final View divider) {
- stripView.addView(divider);
- final LinearLayout.LayoutParams params =
- (LinearLayout.LayoutParams)divider.getLayoutParams();
- params.gravity = Gravity.CENTER;
- }
-
- public void layout(final SuggestedWords suggestedWords, final ViewGroup stripView,
- final ViewGroup placer, final int stripWidth) {
- if (suggestedWords.mIsPunctuationSuggestions) {
- layoutPunctuationSuggestions(suggestedWords, stripView);
- return;
- }
-
- final int countInStrip = mSuggestionsCountInStrip;
- setupTexts(suggestedWords, countInStrip);
- mMoreSuggestionsAvailable = (suggestedWords.size() > countInStrip);
- int x = 0;
- for (int index = 0; index < countInStrip; index++) {
- final int pos = getWordPosition(index, suggestedWords);
-
- if (index != 0) {
- final View divider = mDividers.get(pos);
- // Add divider if this isn't the left most suggestion in suggestions strip.
- addDivider(stripView, divider);
- x += divider.getMeasuredWidth();
- }
-
- final CharSequence styled = mTexts.get(pos);
- final TextView word = mWords.get(pos);
- if (index == mCenterSuggestionIndex && mMoreSuggestionsAvailable) {
- // TODO: This "more suggestions hint" should have nicely designed icon.
- word.setCompoundDrawablesWithIntrinsicBounds(
- null, null, null, mMoreSuggestionsHint);
- // HACK: To align with other TextView that has no compound drawables.
- word.setCompoundDrawablePadding(-mMoreSuggestionsHint.getIntrinsicHeight());
- } else {
- word.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null);
- }
-
- // Disable this suggestion if the suggestion is null or empty.
- word.setEnabled(!TextUtils.isEmpty(styled));
- word.setTextColor(getSuggestionTextColor(index, suggestedWords, pos));
- final int width = getSuggestionWidth(index, stripWidth);
- final CharSequence text = getEllipsizedText(styled, width, word.getPaint());
- final float scaleX = word.getTextScaleX();
- word.setText(text); // TextView.setText() resets text scale x to 1.0.
- word.setTextScaleX(scaleX);
- stripView.addView(word);
- setLayoutWeight(
- word, getSuggestionWeight(index), ViewGroup.LayoutParams.MATCH_PARENT);
- x += word.getMeasuredWidth();
-
- if (DBG && pos < suggestedWords.size()) {
- final String debugInfo = Utils.getDebugInfo(suggestedWords, pos);
- if (debugInfo != null) {
- final TextView info = mInfos.get(pos);
- info.setText(debugInfo);
- placer.addView(info);
- info.measure(ViewGroup.LayoutParams.WRAP_CONTENT,
- ViewGroup.LayoutParams.WRAP_CONTENT);
- final int infoWidth = info.getMeasuredWidth();
- final int y = info.getMeasuredHeight();
- ViewLayoutUtils.placeViewAt(
- info, x - infoWidth, y, infoWidth, info.getMeasuredHeight());
- }
- }
- }
- }
-
- private int getSuggestionWidth(final int index, final int maxWidth) {
- final int paddings = mPadding * mSuggestionsCountInStrip;
- final int dividers = mDividerWidth * (mSuggestionsCountInStrip - 1);
- final int availableWidth = maxWidth - paddings - dividers;
- return (int)(availableWidth * getSuggestionWeight(index));
- }
-
- private float getSuggestionWeight(final int index) {
- if (index == mCenterSuggestionIndex) {
- return mCenterSuggestionWeight;
- } else {
- // TODO: Revisit this for cases of 5 or more suggestions
- return (1.0f - mCenterSuggestionWeight) / (mSuggestionsCountInStrip - 1);
- }
- }
-
- private void setupTexts(final SuggestedWords suggestedWords, final int countInStrip) {
- mTexts.clear();
- final int count = Math.min(suggestedWords.size(), countInStrip);
- for (int pos = 0; pos < count; pos++) {
- final CharSequence styled = getStyledSuggestionWord(suggestedWords, pos);
- mTexts.add(styled);
- }
- for (int pos = count; pos < countInStrip; pos++) {
- // Make this inactive for touches in layout().
- mTexts.add(null);
- }
- }
-
- private void layoutPunctuationSuggestions(final SuggestedWords suggestedWords,
- final ViewGroup stripView) {
- final int countInStrip = Math.min(suggestedWords.size(), PUNCTUATIONS_IN_STRIP);
- for (int index = 0; index < countInStrip; index++) {
- if (index != 0) {
- // Add divider if this isn't the left most suggestion in suggestions strip.
- addDivider(stripView, mDividers.get(index));
- }
-
- final TextView word = mWords.get(index);
- word.setEnabled(true);
- word.setTextColor(mColorAutoCorrect);
- final String text = suggestedWords.getWord(index);
- word.setText(text);
- word.setTextScaleX(1.0f);
- word.setCompoundDrawables(null, null, null, null);
- stripView.addView(word);
- setLayoutWeight(word, 1.0f, mSuggestionsStripHeight);
- }
- mMoreSuggestionsAvailable = false;
- }
-
- public void layoutAddToDictionaryHint(final String word, final ViewGroup stripView,
- final int stripWidth, final CharSequence hintText, final OnClickListener listener) {
- final int width = stripWidth - mDividerWidth - mPadding * 2;
-
- final TextView wordView = mWordToSaveView;
- wordView.setTextColor(mColorTypedWord);
- final int wordWidth = (int)(width * mCenterSuggestionWeight);
- final CharSequence text = getEllipsizedText(word, wordWidth, wordView.getPaint());
- final float wordScaleX = wordView.getTextScaleX();
- wordView.setTag(word);
- wordView.setText(text);
- wordView.setTextScaleX(wordScaleX);
- stripView.addView(wordView);
- setLayoutWeight(wordView, mCenterSuggestionWeight, ViewGroup.LayoutParams.MATCH_PARENT);
-
- stripView.addView(mDividers.get(0));
-
- final TextView leftArrowView = mLeftwardsArrowView;
- leftArrowView.setTextColor(mColorAutoCorrect);
- leftArrowView.setText(LEFTWARDS_ARROW);
- stripView.addView(leftArrowView);
-
- final TextView hintView = mHintToSaveView;
- hintView.setGravity(Gravity.LEFT | Gravity.CENTER_VERTICAL);
- hintView.setTextColor(mColorAutoCorrect);
- final int hintWidth = width - wordWidth - leftArrowView.getWidth();
- final float hintScaleX = getTextScaleX(hintText, hintWidth, hintView.getPaint());
- hintView.setText(hintText);
- hintView.setTextScaleX(hintScaleX);
- stripView.addView(hintView);
- setLayoutWeight(
- hintView, 1.0f - mCenterSuggestionWeight, ViewGroup.LayoutParams.MATCH_PARENT);
-
- wordView.setOnClickListener(listener);
- leftArrowView.setOnClickListener(listener);
- hintView.setOnClickListener(listener);
- }
-
- public CharSequence getAddToDictionaryWord() {
- return (CharSequence)mWordToSaveView.getTag();
- }
-
- public boolean isAddToDictionaryShowing(final View v) {
- return v == mWordToSaveView || v == mHintToSaveView || v == mLeftwardsArrowView;
- }
-
- private static void setLayoutWeight(final View v, final float weight, final int height) {
- final ViewGroup.LayoutParams lp = v.getLayoutParams();
- if (lp instanceof LinearLayout.LayoutParams) {
- final LinearLayout.LayoutParams llp = (LinearLayout.LayoutParams)lp;
- llp.weight = weight;
- llp.width = 0;
- llp.height = height;
- }
- }
-
- private static float getTextScaleX(final CharSequence text, final int maxWidth,
- final TextPaint paint) {
- paint.setTextScaleX(1.0f);
- final int width = getTextWidth(text, paint);
- if (width <= maxWidth) {
- return 1.0f;
- }
- return maxWidth / (float)width;
- }
-
- private static CharSequence getEllipsizedText(final CharSequence text, final int maxWidth,
- final TextPaint paint) {
- if (text == null) return null;
- paint.setTextScaleX(1.0f);
- final int width = getTextWidth(text, paint);
- if (width <= maxWidth) {
- return text;
- }
- final float scaleX = maxWidth / (float)width;
- if (scaleX >= MIN_TEXT_XSCALE) {
- paint.setTextScaleX(scaleX);
- return text;
- }
-
- // Note that TextUtils.ellipsize() use text-x-scale as 1.0 if ellipsize is needed. To
- // get squeezed and ellipsized text, passes enlarged width (maxWidth / MIN_TEXT_XSCALE).
- final CharSequence ellipsized = TextUtils.ellipsize(
- text, paint, maxWidth / MIN_TEXT_XSCALE, TextUtils.TruncateAt.MIDDLE);
- paint.setTextScaleX(MIN_TEXT_XSCALE);
- return ellipsized;
- }
-
- private static int getTextWidth(final CharSequence text, final TextPaint paint) {
- if (TextUtils.isEmpty(text)) return 0;
- final Typeface savedTypeface = paint.getTypeface();
- paint.setTypeface(getTextTypeface(text));
- final int len = text.length();
- final float[] widths = new float[len];
- final int count = paint.getTextWidths(text, 0, len, widths);
- int width = 0;
- for (int i = 0; i < count; i++) {
- width += Math.round(widths[i] + 0.5f);
- }
- paint.setTypeface(savedTypeface);
- return width;
- }
-
- private static Typeface getTextTypeface(final CharSequence text) {
- if (!(text instanceof SpannableString))
- return Typeface.DEFAULT;
-
- final SpannableString ss = (SpannableString)text;
- final StyleSpan[] styles = ss.getSpans(0, text.length(), StyleSpan.class);
- if (styles.length == 0)
- return Typeface.DEFAULT;
-
- switch (styles[0].getStyle()) {
- case Typeface.BOLD: return Typeface.DEFAULT_BOLD;
- // TODO: BOLD_ITALIC, ITALIC case?
- default: return Typeface.DEFAULT;
- }
- }
- }
+ private final SuggestionStripLayoutHelper mLayoutHelper;
/**
* Construct a {@link SuggestionStripView} for showing suggestions to be picked by the user.
@@ -579,19 +93,17 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick
mSuggestionsStrip = (ViewGroup)findViewById(R.id.suggestions_strip);
for (int pos = 0; pos < MAX_SUGGESTIONS; pos++) {
final TextView word = (TextView)inflater.inflate(R.layout.suggestion_word, null);
- word.setTag(pos);
word.setOnClickListener(this);
word.setOnLongClickListener(this);
- mWords.add(word);
+ mWordViews.add(word);
final View divider = inflater.inflate(R.layout.suggestion_divider, null);
- divider.setTag(pos);
divider.setOnClickListener(this);
- mDividers.add(divider);
- mInfos.add((TextView)inflater.inflate(R.layout.suggestion_info, null));
+ mDividerViews.add(divider);
+ mDebugInfoViews.add((TextView)inflater.inflate(R.layout.suggestion_info, null));
}
- mParams = new SuggestionStripViewParams(
- context, attrs, defStyle, mWords, mDividers, mInfos);
+ mLayoutHelper = new SuggestionStripLayoutHelper(
+ context, attrs, defStyle, mWordViews, mDividerViews, mDebugInfoViews);
mMoreSuggestionsContainer = inflater.inflate(R.layout.more_suggestions, null);
mMoreSuggestionsView = (MoreSuggestionsView)mMoreSuggestionsContainer
@@ -617,24 +129,25 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick
public void setSuggestions(final SuggestedWords suggestedWords) {
clear();
mSuggestedWords = suggestedWords;
- mParams.layout(mSuggestedWords, mSuggestionsStrip, this, getWidth());
+ mLayoutHelper.layout(mSuggestedWords, mSuggestionsStrip, this);
if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
ResearchLogger.suggestionStripView_setSuggestions(mSuggestedWords);
}
}
public int setMoreSuggestionsHeight(final int remainingHeight) {
- return mParams.setMoreSuggestionsHeight(remainingHeight);
+ return mLayoutHelper.setMoreSuggestionsHeight(remainingHeight);
}
public boolean isShowingAddToDictionaryHint() {
return mSuggestionsStrip.getChildCount() > 0
- && mParams.isAddToDictionaryShowing(mSuggestionsStrip.getChildAt(0));
+ && mLayoutHelper.isAddToDictionaryShowing(mSuggestionsStrip.getChildAt(0));
}
public void showAddToDictionaryHint(final String word, final CharSequence hintText) {
clear();
- mParams.layoutAddToDictionaryHint(word, mSuggestionsStrip, getWidth(), hintText, this);
+ mLayoutHelper.layoutAddToDictionaryHint(
+ word, mSuggestionsStrip, getWidth(), hintText, this);
}
public boolean dismissAddToDictionaryHint() {
@@ -689,7 +202,8 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick
@Override
public boolean onLongClick(final View view) {
- KeyboardSwitcher.getInstance().hapticAndAudioFeedback(Constants.NOT_A_CODE);
+ AudioAndHapticFeedbackManager.getInstance().hapticAndAudioFeedback(
+ Constants.NOT_A_CODE, this);
return showMoreSuggestions();
}
@@ -698,30 +212,30 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick
if (parentKeyboard == null) {
return false;
}
- final SuggestionStripViewParams params = mParams;
- if (!params.mMoreSuggestionsAvailable) {
+ final SuggestionStripLayoutHelper layoutHelper = mLayoutHelper;
+ if (!layoutHelper.mMoreSuggestionsAvailable) {
return false;
}
final int stripWidth = getWidth();
final View container = mMoreSuggestionsContainer;
final int maxWidth = stripWidth - container.getPaddingLeft() - container.getPaddingRight();
final MoreSuggestions.Builder builder = mMoreSuggestionsBuilder;
- builder.layout(mSuggestedWords, params.mSuggestionsCountInStrip, maxWidth,
- (int)(maxWidth * params.mMinMoreSuggestionsWidth),
- params.getMaxMoreSuggestionsRow(), parentKeyboard);
+ builder.layout(mSuggestedWords, layoutHelper.mSuggestionsCountInStrip, maxWidth,
+ (int)(maxWidth * layoutHelper.mMinMoreSuggestionsWidth),
+ layoutHelper.getMaxMoreSuggestionsRow(), parentKeyboard);
mMoreSuggestionsView.setKeyboard(builder.build());
container.measure(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
final MoreKeysPanel moreKeysPanel = mMoreSuggestionsView;
final int pointX = stripWidth / 2;
- final int pointY = -params.mMoreSuggestionsBottomGap;
+ final int pointY = -layoutHelper.mMoreSuggestionsBottomGap;
moreKeysPanel.showMoreKeysPanel(this, mMoreSuggestionsController, pointX, pointY,
mMoreSuggestionsListener);
mMoreSuggestionsMode = MORE_SUGGESTIONS_CHECKING_MODAL_OR_SLIDING;
mOriginX = mLastX;
mOriginY = mLastY;
- for (int i = 0; i < params.mSuggestionsCountInStrip; i++) {
- mWords.get(i).setPressed(false);
+ for (int i = 0; i < layoutHelper.mSuggestionsCountInStrip; i++) {
+ mWordViews.get(i).setPressed(false);
}
return true;
}
@@ -791,18 +305,20 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick
@Override
public void onClick(final View view) {
- if (mParams.isAddToDictionaryShowing(view)) {
- mListener.addWordToUserDictionary(mParams.getAddToDictionaryWord().toString());
+ if (mLayoutHelper.isAddToDictionaryShowing(view)) {
+ mListener.addWordToUserDictionary(mLayoutHelper.getAddToDictionaryWord().toString());
clear();
return;
}
final Object tag = view.getTag();
- if (!(tag instanceof Integer))
+ if (!(tag instanceof Integer)) {
return;
+ }
final int index = (Integer) tag;
- if (index >= mSuggestedWords.size())
+ if (index >= mSuggestedWords.size()) {
return;
+ }
final SuggestedWordInfo wordInfo = mSuggestedWords.getInfo(index);
mListener.pickSuggestionManually(index, wordInfo);
diff --git a/java/src/com/android/inputmethod/latin/utils/Base64Reader.java b/java/src/com/android/inputmethod/latin/utils/Base64Reader.java
new file mode 100644
index 000000000..3eca6e744
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/utils/Base64Reader.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.inputmethod.latin.utils;
+
+import com.android.inputmethod.annotations.UsedForTesting;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.LineNumberReader;
+
+@UsedForTesting
+public class Base64Reader {
+ private final LineNumberReader mReader;
+
+ private String mLine;
+ private int mCharPos;
+ private int mByteCount;
+
+ @UsedForTesting
+ public Base64Reader(final LineNumberReader reader) {
+ mReader = reader;
+ reset();
+ }
+
+ @UsedForTesting
+ public void reset() {
+ mLine = null;
+ mCharPos = 0;
+ mByteCount = 0;
+ }
+
+ @UsedForTesting
+ public int getLineNumber() {
+ return mReader.getLineNumber();
+ }
+
+ @UsedForTesting
+ public int getByteCount() {
+ return mByteCount;
+ }
+
+ private void fillBuffer() throws IOException {
+ if (mLine == null || mCharPos >= mLine.length()) {
+ mLine = mReader.readLine();
+ mCharPos = 0;
+ }
+ if (mLine == null) {
+ throw new EOFException();
+ }
+ }
+
+ private int peekUint8() throws IOException {
+ fillBuffer();
+ final char c = mLine.charAt(mCharPos);
+ if (c >= 'A' && c <= 'Z')
+ return c - 'A' + 0;
+ if (c >= 'a' && c <= 'z')
+ return c - 'a' + 26;
+ if (c >= '0' && c <= '9')
+ return c - '0' + 52;
+ if (c == '+')
+ return 62;
+ if (c == '/')
+ return 63;
+ if (c == '=')
+ return 0;
+ throw new RuntimeException("Unknown character '" + c + "' in base64 at line "
+ + mReader.getLineNumber());
+ }
+
+ private int getUint8() throws IOException {
+ final int value = peekUint8();
+ mCharPos++;
+ return value;
+ }
+
+ @UsedForTesting
+ public int readUint8() throws IOException {
+ final int value1, value2;
+ switch (mByteCount % 3) {
+ case 0:
+ value1 = getUint8() << 2;
+ value2 = value1 | (peekUint8() >> 4);
+ break;
+ case 1:
+ value1 = (getUint8() & 0x0f) << 4;
+ value2 = value1 | (peekUint8() >> 2);
+ break;
+ default:
+ value1 = (getUint8() & 0x03) << 6;
+ value2 = value1 | getUint8();
+ break;
+ }
+ mByteCount++;
+ return value2;
+ }
+
+ @UsedForTesting
+ public short readInt16() throws IOException {
+ final int data = readUint8() << 8;
+ return (short)(data | readUint8());
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/utils/CsvUtils.java b/java/src/com/android/inputmethod/latin/utils/CsvUtils.java
new file mode 100644
index 000000000..999c2f0de
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/utils/CsvUtils.java
@@ -0,0 +1,319 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.inputmethod.latin.utils;
+
+import com.android.inputmethod.annotations.UsedForTesting;
+import com.android.inputmethod.latin.CollectionUtils;
+
+import java.util.ArrayList;
+
+/**
+ * Utility methods for parsing and serializing Comma-Separated Values. The public APIs of this
+ * utility class are {@link #split(String)}, {@link #split(int,String)}, {@link #join(String)},
+ * {@link #join(int,String...)}, and {@link #join(int,int[],String...)}.
+ *
+ * This class implements CSV parsing and serializing methods conforming to RFC 4180 with an
+ * exception:
+ * These methods can't handle new line code escaped in double quotes.
+ */
+@UsedForTesting
+public final class CsvUtils {
+ private CsvUtils() {
+ // This utility class is not publicly instantiable.
+ }
+
+ public static final int SPLIT_FLAGS_NONE = 0x0;
+ /**
+ * A flag for {@link #split(int,String)}. If this flag is specified, the method will trim
+ * spaces around fields before splitting. Note that this behavior doesn't conform to RFC 4180.
+ */
+ public static final int SPLIT_FLAGS_TRIM_SPACES = 0x1;
+
+ public static final int JOIN_FLAGS_NONE = 0x0;
+ /**
+ * A flag for {@link #join(int,String...)} and {@link #join(int,int[],String...)}. If this
+ * flag is specified, these methods surround each field with double quotes before joining.
+ */
+ public static final int JOIN_FLAGS_ALWAYS_QUOTED = 0x1;
+ /**
+ * A flag for {@link #join(int,String...)} and {@link #join(int,int[],String...)}. If this
+ * flag is specified, these methods add an extra space just after the comma separator. Note that
+ * this behavior doesn't conform to RFC 4180.
+ */
+ public static final int JOIN_FLAGS_EXTRA_SPACE = 0x2;
+
+ // Note that none of these characters match high or low surrogate characters, so we need not
+ // take care of matching by code point.
+ private static final char COMMA = ',';
+ private static final char SPACE = ' ';
+ private static final char QUOTE = '"';
+
+ @SuppressWarnings("serial")
+ public static class CsvParseException extends RuntimeException {
+ public CsvParseException(final String message) {
+ super(message);
+ }
+ }
+
+ /**
+ * Find the first non-space character in the text.
+ *
+ * @param text the text to be searched.
+ * @param fromIndex the index to start the search from, inclusive.
+ * @return the index of the first occurrence of the non-space character in the
+ * <code>text</code> that is greater than or equal to <code>fromIndex</code>, or the length of
+ * the <code>text</code> if the character does not occur.
+ */
+ private static int indexOfNonSpace(final String text, final int fromIndex) {
+ final int length = text.length();
+ if (fromIndex < 0 || fromIndex > length) {
+ throw new IllegalArgumentException("text=" + text + " fromIndex=" + fromIndex);
+ }
+ int index = fromIndex;
+ while (index < length && text.charAt(index) == SPACE) {
+ index++;
+ }
+ return index;
+ }
+
+ /**
+ * Find the last non-space character in the text.
+ *
+ * @param text the text to be searched.
+ * @param fromIndex the index to start the search from, exclusive.
+ * @param toIndex the index to end the search at, inclusive. Usually <code>toIndex</code>
+ * points a non-space character.
+ * @return the index of the last occurrence of the non-space character in the
+ * <code>text</code>, exclusive. It is less than <code>fromIndex</code> and greater than
+ * <code>toIndex</code>, or <code>toIndex</code> if the character does not occur.
+ */
+ private static int lastIndexOfNonSpace(final String text, final int fromIndex,
+ final int toIndex) {
+ if (toIndex < 0 || fromIndex > text.length() || fromIndex < toIndex) {
+ throw new IllegalArgumentException(
+ "text=" + text + " fromIndex=" + fromIndex + " toIndex=" + toIndex);
+ }
+ int index = fromIndex;
+ while (index > toIndex && text.charAt(index - 1) == SPACE) {
+ index--;
+ }
+ return index;
+ }
+
+ /**
+ * Find the index of a comma separator. The search takes account of quoted fields and escape
+ * quotes.
+ *
+ * @param text the text to be searched.
+ * @param fromIndex the index to start the search from, inclusive.
+ * @return the index of the comma separator, exclusive.
+ */
+ private static int indexOfSeparatorComma(final String text, final int fromIndex) {
+ final int length = text.length();
+ if (fromIndex < 0 || fromIndex > length) {
+ throw new IllegalArgumentException("text=" + text + " fromIndex=" + fromIndex);
+ }
+ final boolean isQuoted = (length - fromIndex > 0 && text.charAt(fromIndex) == QUOTE);
+ for (int index = fromIndex + (isQuoted ? 1 : 0); index < length; index++) {
+ final char c = text.charAt(index);
+ if (c == COMMA && !isQuoted) {
+ return index;
+ }
+ if (c == QUOTE) {
+ final int nextIndex = index + 1;
+ if (nextIndex < length && text.charAt(nextIndex) == QUOTE) {
+ // Quoted quote.
+ index = nextIndex;
+ continue;
+ }
+ // Closing quote.
+ final int endIndex = text.indexOf(COMMA, nextIndex);
+ return endIndex < 0 ? length : endIndex;
+ }
+ }
+ return length;
+ }
+
+ /**
+ * Removing any enclosing QUOTEs (U+0022), and convert any two consecutive QUOTEs into
+ * one QUOTE.
+ *
+ * @param text the CSV field text that may have enclosing QUOTEs and escaped QUOTE character.
+ * @return the text that has been removed enclosing quotes and converted two consecutive QUOTEs
+ * into one QUOTE.
+ */
+ @UsedForTesting
+ /* private */ static String unescapeField(final String text) {
+ StringBuilder sb = null;
+ final int length = text.length();
+ final boolean isQuoted = (length > 0 && text.charAt(0) == QUOTE);
+ int start = isQuoted ? 1 : 0;
+ int end = start;
+ while (start <= length && (end = text.indexOf(QUOTE, start)) >= start) {
+ final int nextIndex = end + 1;
+ if (nextIndex == length && isQuoted) {
+ // Closing quote.
+ break;
+ }
+ if (nextIndex < length && text.charAt(nextIndex) == QUOTE) {
+ if (!isQuoted) {
+ throw new CsvParseException("Escaped quote in text");
+ }
+ // Quoted quote.
+ if (sb == null) {
+ sb = new StringBuilder();
+ }
+ sb.append(text.substring(start, nextIndex));
+ start = nextIndex + 1;
+ } else {
+ throw new CsvParseException(
+ isQuoted ? "Raw quote in quoted text" : "Raw quote in text");
+ }
+ }
+ if (end < 0 && isQuoted) {
+ throw new CsvParseException("Unterminated quote");
+ }
+ if (end < 0) {
+ end = length;
+ }
+ if (sb != null && start < length) {
+ sb.append(text.substring(start, end));
+ }
+ return sb == null ? text.substring(start, end) : sb.toString();
+ }
+
+ /**
+ * Split the CSV text into fields. The leading and trailing spaces of the each field can be
+ * trimmed optionally.
+ *
+ * @param splitFlags flags for split behavior. {@link #SPLIT_FLAGS_TRIM_SPACES} will trim
+ * spaces around each fields.
+ * @param line the text of CSV fields.
+ * @return the array of unescaped CVS fields.
+ * @throws CsvParseException
+ */
+ @UsedForTesting
+ public static String[] split(final int splitFlags, final String line) throws CsvParseException {
+ final boolean trimSpaces = (splitFlags & SPLIT_FLAGS_TRIM_SPACES) != 0;
+ final ArrayList<String> fields = CollectionUtils.newArrayList();
+ final int length = line.length();
+ int start = 0;
+ do {
+ final int csvStart = trimSpaces ? indexOfNonSpace(line, start) : start;
+ final int end = indexOfSeparatorComma(line, csvStart);
+ final int csvEnd = trimSpaces ? lastIndexOfNonSpace(line, end, csvStart) : end;
+ final String csvText = unescapeField(line.substring(csvStart, csvEnd));
+ fields.add(csvText);
+ start = end + 1;
+ } while (start <= length);
+ return fields.toArray(new String[fields.size()]);
+ }
+
+ @UsedForTesting
+ public static String[] split(final String line) throws CsvParseException {
+ return split(SPLIT_FLAGS_NONE, line);
+ }
+
+ /**
+ * Convert the raw CSV field text to the escaped text. It adds enclosing QUOTEs (U+0022) if the
+ * raw value contains any QUOTE or comma. Also it converts any QUOTE character into two
+ * consecutive QUOTE characters.
+ *
+ * @param text the raw CSV field text to be escaped.
+ * @param alwaysQuoted true if the escaped text should always be enclosed by QUOTEs.
+ * @return the escaped text.
+ */
+ @UsedForTesting
+ /* private */ static String escapeField(final String text, final boolean alwaysQuoted) {
+ StringBuilder sb = null;
+ boolean needsQuoted = alwaysQuoted;
+ final int length = text.length();
+ int indexToBeAppended = 0;
+ for (int index = indexToBeAppended; index < length; index++) {
+ final char c = text.charAt(index);
+ if (c == COMMA) {
+ needsQuoted = true;
+ } else if (c == QUOTE) {
+ needsQuoted = true;
+ if (sb == null) {
+ sb = new StringBuilder();
+ }
+ sb.append(text.substring(indexToBeAppended, index));
+ indexToBeAppended = index + 1;
+ sb.append(QUOTE); // escaping quote.
+ sb.append(QUOTE); // escaped quote.
+ }
+ }
+ if (sb != null && indexToBeAppended < length) {
+ sb.append(text.substring(indexToBeAppended));
+ }
+ final String escapedText = (sb == null) ? text : sb.toString();
+ return needsQuoted ? QUOTE + escapedText + QUOTE : escapedText;
+ }
+
+ private static final String SPACES = " ";
+
+ private static void padToColumn(final StringBuilder sb, final int column) {
+ int padding;
+ while ((padding = column - sb.length()) > 0) {
+ final String spaces = SPACES.substring(0, Math.min(padding, SPACES.length()));
+ sb.append(spaces);
+ }
+ }
+
+ /**
+ * Join CSV text fields with comma. The column positions of the fields can be specified
+ * optionally. Surround each fields with double quotes before joining.
+ *
+ * @param joinFlags flags for join behavior. {@link #JOIN_FLAGS_EXTRA_SPACE} will add an extra
+ * space after each comma separator. {@link #JOIN_FLAGS_ALWAYS_QUOTED} will always add
+ * surrounding quotes to each element.
+ * @param columnPositions the array of column positions of the fields. It can be shorter than
+ * <code>fields</code> or null. Note that specifying the array column positions of the fields
+ * doesn't conform to RFC 4180.
+ * @param fields the CSV text fields.
+ * @return the string of the joined and escaped <code>fields</code>.
+ */
+ @UsedForTesting
+ public static String join(final int joinFlags, final int columnPositions[],
+ final String... fields) {
+ final boolean alwaysQuoted = (joinFlags & JOIN_FLAGS_ALWAYS_QUOTED) != 0;
+ final String separator = COMMA + ((joinFlags & JOIN_FLAGS_EXTRA_SPACE) != 0 ? " " : "");
+ final StringBuilder sb = new StringBuilder();
+ for (int index = 0; index < fields.length; index++) {
+ if (index > 0) {
+ sb.append(separator);
+ }
+ if (columnPositions != null && index < columnPositions.length) {
+ padToColumn(sb, columnPositions[index]);
+ }
+ final String escapedText = escapeField(fields[index], alwaysQuoted);
+ sb.append(escapedText);
+ }
+ return sb.toString();
+ }
+
+ @UsedForTesting
+ public static String join(final int joinFlags, final String... fields) {
+ return join(joinFlags, null, fields);
+ }
+
+ @UsedForTesting
+ public static String join(final String... fields) {
+ return join(JOIN_FLAGS_NONE, null, fields);
+ }
+}
diff --git a/java/src/com/android/inputmethod/research/FeedbackLog.java b/java/src/com/android/inputmethod/research/FeedbackLog.java
new file mode 100644
index 000000000..5af194c32
--- /dev/null
+++ b/java/src/com/android/inputmethod/research/FeedbackLog.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.inputmethod.research;
+
+import android.content.Context;
+
+import java.io.File;
+
+public class FeedbackLog extends ResearchLog {
+ public FeedbackLog(final File outputFile, final Context context) {
+ super(outputFile, context);
+ }
+
+ @Override
+ public boolean isFeedbackLog() {
+ return true;
+ }
+}
diff --git a/java/src/com/android/inputmethod/research/LogUnit.java b/java/src/com/android/inputmethod/research/LogUnit.java
index cf1388f46..164c7e8cc 100644
--- a/java/src/com/android/inputmethod/research/LogUnit.java
+++ b/java/src/com/android/inputmethod/research/LogUnit.java
@@ -67,7 +67,7 @@ public class LogUnit {
private String[] mWordArray = EMPTY_STRING_ARRAY;
private boolean mMayContainDigit;
private boolean mIsPartOfMegaword;
- private boolean mContainsCorrection;
+ private boolean mContainsUserDeletions;
// mCorrectionType indicates whether the word was corrected at all, and if so, the nature of the
// correction.
@@ -277,13 +277,13 @@ public class LogUnit {
}
// TODO: Refactor to eliminate getter/setters
- public void setContainsCorrection() {
- mContainsCorrection = true;
+ public void setContainsUserDeletions() {
+ mContainsUserDeletions = true;
}
// TODO: Refactor to eliminate getter/setters
- public boolean containsCorrection() {
- return mContainsCorrection;
+ public boolean containsUserDeletions() {
+ return mContainsUserDeletions;
}
// TODO: Refactor to eliminate getter/setters
@@ -323,7 +323,7 @@ public class LogUnit {
true /* isPartOfMegaword */);
newLogUnit.mWords = null;
newLogUnit.mMayContainDigit = mMayContainDigit;
- newLogUnit.mContainsCorrection = mContainsCorrection;
+ newLogUnit.mContainsUserDeletions = mContainsUserDeletions;
// Purge the logStatements and associated data from this LogUnit.
laterLogStatements.clear();
@@ -346,7 +346,7 @@ public class LogUnit {
setWords(logUnit.mWords);
}
mMayContainDigit = mMayContainDigit || logUnit.mMayContainDigit;
- mContainsCorrection = mContainsCorrection || logUnit.mContainsCorrection;
+ mContainsUserDeletions = mContainsUserDeletions || logUnit.mContainsUserDeletions;
mIsPartOfMegaword = false;
}
diff --git a/java/src/com/android/inputmethod/research/MainLogBuffer.java b/java/src/com/android/inputmethod/research/MainLogBuffer.java
index 9aa349906..3482153b4 100644
--- a/java/src/com/android/inputmethod/research/MainLogBuffer.java
+++ b/java/src/com/android/inputmethod/research/MainLogBuffer.java
@@ -63,6 +63,15 @@ public abstract class MainLogBuffer extends FixedLogBuffer {
private static final boolean DEBUG = false
&& ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS_DEBUG;
+ // Keep consistent with switch statement in Statistics.recordPublishabilityResultCode()
+ public static final int PUBLISHABILITY_PUBLISHABLE = 0;
+ public static final int PUBLISHABILITY_UNPUBLISHABLE_STOPPING = 1;
+ public static final int PUBLISHABILITY_UNPUBLISHABLE_INCORRECT_WORD_COUNT = 2;
+ public static final int PUBLISHABILITY_UNPUBLISHABLE_SAMPLED_TOO_RECENTLY = 3;
+ public static final int PUBLISHABILITY_UNPUBLISHABLE_DICTIONARY_UNAVAILABLE = 4;
+ public static final int PUBLISHABILITY_UNPUBLISHABLE_MAY_CONTAIN_DIGIT = 5;
+ public static final int PUBLISHABILITY_UNPUBLISHABLE_NOT_IN_DICTIONARY = 6;
+
// The size of the n-grams logged. E.g. N_GRAM_SIZE = 2 means to sample bigrams.
public static final int N_GRAM_SIZE = 2;
@@ -105,21 +114,24 @@ public abstract class MainLogBuffer extends FixedLogBuffer {
}
/**
- * Determines whether uploading the n words at the front the MainLogBuffer will not violate
- * user privacy.
+ * Determines whether the string determined by a series of LogUnits will not violate user
+ * privacy if published.
+ *
+ * @param logUnits a LogUnit list to check for publishability
+ * @param nGramSize the smallest n-gram acceptable to be published. if
+ * {@link ResearchLogger.IS_LOGGING_EVERYTHING} is true, then publish if there are more than
+ * {@code minNGramSize} words in the logUnits, otherwise wait. if {@link
+ * ResearchLogger.IS_LOGGING_EVERYTHING} is false, then ensure that there are exactly nGramSize
+ * words in the LogUnits.
*
- * The size of the MainLogBuffer is just enough to hold one n-gram, its corrections, and any
- * non-character data that is typed between words. The decision about privacy is made based on
- * the buffer's entire content. If it is decided that the privacy risks are too great to upload
- * the contents of this buffer, a censored version of the LogItems may still be uploaded. E.g.,
- * the screen orientation and other characteristics about the device can be uploaded without
- * revealing much about the user.
+ * @return one of the {@code PUBLISHABILITY_*} result codes defined in this class.
*/
- private boolean isSafeNGram(final ArrayList<LogUnit> logUnits, final int minNGramSize) {
+ private int getPublishabilityResultCode(final ArrayList<LogUnit> logUnits,
+ final int nGramSize) {
// Bypass privacy checks when debugging.
if (ResearchLogger.IS_LOGGING_EVERYTHING) {
if (mIsStopping) {
- return true;
+ return PUBLISHABILITY_UNPUBLISHABLE_STOPPING;
}
// Only check that it is the right length. If not, wait for later words to make
// complete n-grams.
@@ -129,13 +141,17 @@ public abstract class MainLogBuffer extends FixedLogBuffer {
final LogUnit logUnit = logUnits.get(i);
numWordsInLogUnitList += logUnit.getNumWords();
}
- return numWordsInLogUnitList >= minNGramSize;
+ if (numWordsInLogUnitList >= nGramSize) {
+ return PUBLISHABILITY_PUBLISHABLE;
+ } else {
+ return PUBLISHABILITY_UNPUBLISHABLE_INCORRECT_WORD_COUNT;
+ }
}
// Check that we are not sampling too frequently. Having sampled recently might disclose
// too much of the user's intended meaning.
if (mNumWordsUntilSafeToSample > 0) {
- return false;
+ return PUBLISHABILITY_UNPUBLISHABLE_SAMPLED_TOO_RECENTLY;
}
// Reload the dictionary in case it has changed (e.g., because the user has changed
// languages).
@@ -144,7 +160,7 @@ public abstract class MainLogBuffer extends FixedLogBuffer {
// Main dictionary is unavailable. Since we cannot check it, we cannot tell if a
// word is out-of-vocabulary or not. Therefore, we must judge the entire buffer
// contents to potentially pose a privacy risk.
- return false;
+ return PUBLISHABILITY_UNPUBLISHABLE_DICTIONARY_UNAVAILABLE;
}
// Check each word in the buffer. If any word poses a privacy threat, we cannot upload
@@ -155,7 +171,7 @@ public abstract class MainLogBuffer extends FixedLogBuffer {
if (!logUnit.hasOneOrMoreWords()) {
// Digits outside words are a privacy threat.
if (logUnit.mayContainDigit()) {
- return false;
+ return PUBLISHABILITY_UNPUBLISHABLE_MAY_CONTAIN_DIGIT;
}
} else {
numWordsInLogUnitList += logUnit.getNumWords();
@@ -168,14 +184,18 @@ public abstract class MainLogBuffer extends FixedLogBuffer {
+ ResearchLogger.hasLetters(word)
+ ", isValid: " + (dictionary.isValidWord(word)));
}
- return false;
+ return PUBLISHABILITY_UNPUBLISHABLE_NOT_IN_DICTIONARY;
}
}
}
}
// Finally, only return true if the ngram is the right size.
- return numWordsInLogUnitList == minNGramSize;
+ if (numWordsInLogUnitList == nGramSize) {
+ return PUBLISHABILITY_PUBLISHABLE;
+ } else {
+ return PUBLISHABILITY_UNPUBLISHABLE_INCORRECT_WORD_COUNT;
+ }
}
public void shiftAndPublishAll() throws IOException {
@@ -196,11 +216,29 @@ public abstract class MainLogBuffer extends FixedLogBuffer {
}
}
+ /**
+ * If there is a safe n-gram at the front of this log buffer, publish it with all details, and
+ * remove the LogUnits that constitute it.
+ *
+ * An n-gram might not be "safe" if it violates privacy controls. E.g., it might contain
+ * numbers, an out-of-vocabulary word, or another n-gram may have been published recently. If
+ * there is no safe n-gram, then the LogUnits up through the first word-containing LogUnit are
+ * published, but without disclosing any privacy-related details, such as the word the LogUnit
+ * generated, motion data, etc.
+ *
+ * Note that a LogUnit can hold more than one word if the user types without explicit spaces.
+ * In this case, the words may be grouped together in such a way that pulling an n-gram off the
+ * front would require splitting a LogUnit. Splitting a LogUnit is not possible, so this case
+ * is treated just as the unsafe n-gram case. This may cause n-grams to be sampled at slightly
+ * less than the target frequency.
+ */
protected final void publishLogUnitsAtFrontOfBuffer() throws IOException {
// TODO: Refactor this method to require fewer passes through the LogUnits. Should really
// require only one pass.
ArrayList<LogUnit> logUnits = peekAtFirstNWords(N_GRAM_SIZE);
- if (isSafeNGram(logUnits, N_GRAM_SIZE)) {
+ final int publishabilityResultCode = getPublishabilityResultCode(logUnits, N_GRAM_SIZE);
+ ResearchLogger.recordPublishabilityResultCode(publishabilityResultCode);
+ if (publishabilityResultCode == MainLogBuffer.PUBLISHABILITY_PUBLISHABLE) {
// Good n-gram at the front of the buffer. Publish it, disclosing details.
publish(logUnits, true /* canIncludePrivateData */);
shiftOutWords(N_GRAM_SIZE);
diff --git a/java/src/com/android/inputmethod/research/ResearchLog.java b/java/src/com/android/inputmethod/research/ResearchLog.java
index 3e82139a6..fde2798e1 100644
--- a/java/src/com/android/inputmethod/research/ResearchLog.java
+++ b/java/src/com/android/inputmethod/research/ResearchLog.java
@@ -81,6 +81,17 @@ public class ResearchLog {
}
/**
+ * Returns true if this is a FeedbackLog.
+ *
+ * FeedbackLogs record only the data associated with a Feedback dialog. Instead of normal
+ * logging, they contain a LogStatement with the complete feedback string and optionally a
+ * recording of the user's supplied demo of the problem.
+ */
+ public boolean isFeedbackLog() {
+ return false;
+ }
+
+ /**
* Waits for any publication requests to finish and closes the {@link JsonWriter} used for
* output.
*
diff --git a/java/src/com/android/inputmethod/research/ResearchLogger.java b/java/src/com/android/inputmethod/research/ResearchLogger.java
index 8b8ea21e9..aa4a866b8 100644
--- a/java/src/com/android/inputmethod/research/ResearchLogger.java
+++ b/java/src/com/android/inputmethod/research/ResearchLogger.java
@@ -83,6 +83,8 @@ import java.util.List;
import java.util.Random;
import java.util.regex.Pattern;
+// TODO: Add a unit test for every "logging" method (i.e. that is called from the IME and calls
+// enqueueEvent to record a LogStatement).
/**
* Logs the use of the LatinIME keyboard.
*
@@ -194,10 +196,17 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
// gesture, and when committing the earlier word, split the LogUnit.
private long mSavedDownEventTime;
private Bundle mFeedbackDialogBundle = null;
+ // Whether the feedback dialog is visible, and the user is typing into it. Normal logging is
+ // not performed on text that the user types into the feedback dialog.
private boolean mInFeedbackDialog = false;
private Handler mUserRecordingTimeoutHandler;
private static final long USER_RECORDING_TIMEOUT_MS = 30L * DateUtils.SECOND_IN_MILLIS;
+ // Stores a temporary LogUnit while generating a phantom space. Needed because phantom spaces
+ // are issued out-of-order, immediately before the characters generated by other operations that
+ // have already outputted LogStatements.
+ private LogUnit mPhantomSpaceLogUnit = null;
+
private ResearchLogger() {
mStatistics = Statistics.getInstance();
}
@@ -253,14 +262,14 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
if (DEBUG) {
final String wordsString = logUnit.getWordsAsString();
Log.d(TAG, "onPublish: '" + wordsString
- + "', hc: " + logUnit.containsCorrection()
+ + "', hc: " + logUnit.containsUserDeletions()
+ ", cipd: " + canIncludePrivateData);
}
for (final String word : logUnit.getWordsAsStringArray()) {
final Dictionary dictionary = getDictionary();
mStatistics.recordWordEntered(
dictionary != null && dictionary.isValidWord(word),
- logUnit.containsCorrection());
+ logUnit.containsUserDeletions());
}
}
publishLogUnits(logUnits, mMainResearchLog, canIncludePrivateData);
@@ -650,7 +659,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
feedbackLogUnit.addLogStatement(LOGSTATEMENT_FEEDBACK, SystemClock.uptimeMillis(),
feedbackContents, accountName, recording);
- final ResearchLog feedbackLog = new ResearchLog(mResearchLogDirectory.getLogFilePath(
+ final ResearchLog feedbackLog = new FeedbackLog(mResearchLogDirectory.getLogFilePath(
System.currentTimeMillis(), System.nanoTime()), mLatinIME);
final LogBuffer feedbackLogBuffer = new LogBuffer();
feedbackLogBuffer.shiftIn(feedbackLogUnit);
@@ -713,8 +722,28 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
mIsPasswordView = isPasswordView;
}
- private boolean isAllowedToLog() {
- return !mIsPasswordView && sIsLogging && !mInFeedbackDialog;
+ /**
+ * Returns true if logging is permitted.
+ *
+ * This method is called when adding a LogStatement to a LogUnit, and when adding a LogUnit to a
+ * ResearchLog. It is checked in both places in case conditions change between these times, and
+ * as a defensive measure in case refactoring changes the logging pipeline.
+ */
+ private boolean isAllowedToLogTo(final ResearchLog researchLog) {
+ // Logging is never allowed in these circumstances
+ if (mIsPasswordView) return false;
+ if (!sIsLogging) return false;
+ if (mInFeedbackDialog) {
+ // The FeedbackDialog is up. Normal logging should not happen (the user might be trying
+ // out things while the dialog is up, and their reporting of an issue may not be
+ // representative of what they normally type). However, after the user has finished
+ // entering their feedback, the logger packs their comments and an encoded version of
+ // any demonstration of the issue into a special "FeedbackLog". So if the FeedbackLog
+ // is the destination, we do want to allow logging to it.
+ return researchLog.isFeedbackLog();
+ }
+ // No other exclusions. Logging is permitted.
+ return true;
}
public void requestIndicatorRedraw() {
@@ -747,7 +776,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
// and remove this method.
// The check for MainKeyboardView ensures that the indicator only decorates the main
// keyboard, not every keyboard.
- if (IS_SHOWING_INDICATOR && (isAllowedToLog() || isReplaying())
+ if (IS_SHOWING_INDICATOR && (isAllowedToLogTo(mMainResearchLog) || isReplaying())
&& view instanceof MainKeyboardView) {
final int savedColor = paint.getColor();
paint.setColor(getIndicatorColor());
@@ -782,7 +811,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
private synchronized void enqueueEvent(final LogUnit logUnit, final LogStatement logStatement,
final Object... values) {
assert values.length == logStatement.getKeys().length;
- if (isAllowedToLog() && logUnit != null) {
+ if (isAllowedToLogTo(mMainResearchLog) && logUnit != null) {
final long time = SystemClock.uptimeMillis();
logUnit.addLogStatement(logStatement, time, values);
}
@@ -792,8 +821,8 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
mCurrentLogUnit.setMayContainDigit();
}
- private void setCurrentLogUnitContainsCorrection() {
- mCurrentLogUnit.setContainsCorrection();
+ private void setCurrentLogUnitContainsUserDeletions() {
+ mCurrentLogUnit.setContainsUserDeletions();
}
private void setCurrentLogUnitCorrectionType(final int correctionType) {
@@ -825,20 +854,22 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
// The user has deleted this word and returned to the previous. Check that the word in the
// logUnit matches the expected word. If so, restore the last log unit committed to be the
// current logUnit. I.e., pull out the last LogUnit from all the LogBuffers, and make
- // restore it to mCurrentLogUnit so the new edits are captured with the word. Optionally
- // dump the contents of mCurrentLogUnit (useful if they contain deletions of the next word
- // that should not be reported to protect user privacy)
+ // it the mCurrentLogUnit so the new edits are captured with the word. Optionally dump the
+ // contents of mCurrentLogUnit (useful if they contain deletions of the next word that
+ // should not be reported to protect user privacy)
//
// Note that we don't use mLastLogUnit here, because it only goes one word back and is only
// needed for reverts, which only happen one back.
final LogUnit oldLogUnit = mMainLogBuffer.peekLastLogUnit();
- // Check that expected word matches.
+ // Check that expected word matches. It's ok if both strings are null, because this is the
+ // case where the LogUnit is storing a non-word, e.g. a separator.
if (oldLogUnit != null) {
+ // Because the word is stored in the LogUnit with digits scrubbed, the comparison must
+ // be made on a scrubbed version of the expectedWord as well.
+ final String scrubbedExpectedWord = scrubDigitsFromString(expectedWord);
final String oldLogUnitWords = oldLogUnit.getWordsAsString();
- if (oldLogUnitWords != null && !oldLogUnitWords.equals(expectedWord)) {
- return;
- }
+ if (!TextUtils.equals(scrubbedExpectedWord, oldLogUnitWords)) return;
}
// Uncommit, merging if necessary.
@@ -881,7 +912,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
final ResearchLog researchLog, final boolean canIncludePrivateData) {
final LogUnit openingLogUnit = new LogUnit();
if (logUnits.isEmpty()) return;
- if (!isAllowedToLog()) return;
+ if (!isAllowedToLogTo(researchLog)) return;
// LogUnits not containing private data, such as contextual data for the log, do not require
// logSegment boundary statements.
if (canIncludePrivateData) {
@@ -893,7 +924,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
if (DEBUG) {
Log.d(TAG, "publishLogBuffer: " + (logUnit.hasOneOrMoreWords()
? logUnit.getWordsAsString() : "<wordless>")
- + ", correction?: " + logUnit.containsCorrection());
+ + ", correction?: " + logUnit.containsUserDeletions());
}
researchLog.publish(logUnit, canIncludePrivateData);
}
@@ -954,7 +985,8 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
return Character.isDigit(codePoint) ? DIGIT_REPLACEMENT_CODEPOINT : codePoint;
}
- /* package for test */ static String scrubDigitsFromString(String s) {
+ /* package for test */ static String scrubDigitsFromString(final String s) {
+ if (s == null) return null;
StringBuilder sb = null;
final int length = s.length();
for (int i = 0; i < length; i = s.offsetByCodePoints(i, 1)) {
@@ -1247,6 +1279,16 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
}
/**
+ * Log a revert of onTextInput() (known in the IME as "EnteredText").
+ *
+ * SystemResponse: Remove the LogUnit recording the textInput
+ */
+ public static void latinIME_handleBackspace_cancelTextInput(final String text) {
+ final ResearchLogger researchLogger = getInstance();
+ researchLogger.uncommitCurrentLogUnit(text, true /* dumpCurrentLogUnit */);
+ }
+
+ /**
* Log a call to LatinIME.pickSuggestionManually().
*
* UserAction: The user has chosen a specific word from the suggestion strip.
@@ -1259,7 +1301,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
final ResearchLogger researchLogger = getInstance();
if (!replacedWord.equals(suggestion.toString())) {
// The user chose something other than what was already there.
- researchLogger.setCurrentLogUnitContainsCorrection();
+ researchLogger.setCurrentLogUnitContainsUserDeletions();
researchLogger.setCurrentLogUnitCorrectionType(LogUnit.CORRECTIONTYPE_TYPO);
}
final String scrubbedWord = scrubDigitsFromString(suggestion);
@@ -1291,17 +1333,32 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
/**
* Log a call to LatinIME.sendKeyCodePoint().
*
- * SystemResponse: The IME is inserting text into the TextView for numbers, fixed strings, or
- * some other unusual mechanism.
+ * SystemResponse: The IME is inserting text into the TextView for non-word-constituent,
+ * strings (separators, numbers, other symbols).
*/
private static final LogStatement LOGSTATEMENT_LATINIME_SENDKEYCODEPOINT =
new LogStatement("LatinIMESendKeyCodePoint", true, false, "code");
public static void latinIME_sendKeyCodePoint(final int code) {
final ResearchLogger researchLogger = getInstance();
- researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_SENDKEYCODEPOINT,
- Constants.printableCode(scrubDigitFromCodePoint(code)));
- if (Character.isDigit(code)) {
- researchLogger.setCurrentLogUnitContainsDigitFlag();
+ final LogUnit phantomSpaceLogUnit = researchLogger.mPhantomSpaceLogUnit;
+ if (phantomSpaceLogUnit == null) {
+ researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_SENDKEYCODEPOINT,
+ Constants.printableCode(scrubDigitFromCodePoint(code)));
+ if (Character.isDigit(code)) {
+ researchLogger.setCurrentLogUnitContainsDigitFlag();
+ }
+ researchLogger.commitCurrentLogUnit();
+ } else {
+ researchLogger.enqueueEvent(phantomSpaceLogUnit, LOGSTATEMENT_LATINIME_SENDKEYCODEPOINT,
+ Constants.printableCode(scrubDigitFromCodePoint(code)));
+ if (Character.isDigit(code)) {
+ phantomSpaceLogUnit.setMayContainDigit();
+ }
+ researchLogger.mMainLogBuffer.shiftIn(phantomSpaceLogUnit);
+ if (researchLogger.mUserRecordingLogBuffer != null) {
+ researchLogger.mUserRecordingLogBuffer.shiftIn(phantomSpaceLogUnit);
+ }
+ researchLogger.mPhantomSpaceLogUnit = null;
}
}
@@ -1311,12 +1368,18 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
* SystemResponse: The IME is inserting a real space in place of a phantom space.
*/
private static final LogStatement LOGSTATEMENT_LATINIME_PROMOTEPHANTOMSPACE =
- new LogStatement("LatinIMEPromotPhantomSpace", false, false);
+ new LogStatement("LatinIMEPromotePhantomSpace", false, false);
public static void latinIME_promotePhantomSpace() {
+ // A phantom space is always added before the text that triggered it. The triggering text
+ // and the events that created it will be in mCurrentLogUnit, but the phantom space should
+ // be in its own LogUnit, committed before the triggering text. Although it is created
+ // here, it is not added to the LogBuffer until the following call to
+ // latinIME_sendKeyCodePoint, because SENDKEYCODEPOINT LogStatement also must go into that
+ // LogUnit.
final ResearchLogger researchLogger = getInstance();
- final LogUnit logUnit;
- logUnit = researchLogger.mMainLogBuffer.peekLastLogUnit();
- researchLogger.enqueueEvent(logUnit, LOGSTATEMENT_LATINIME_PROMOTEPHANTOMSPACE);
+ researchLogger.mPhantomSpaceLogUnit = new LogUnit();
+ researchLogger.enqueueEvent(researchLogger.mPhantomSpaceLogUnit,
+ LOGSTATEMENT_LATINIME_PROMOTEPHANTOMSPACE);
}
/**
@@ -1402,23 +1465,40 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
public static void latinIME_revertCommit(final String committedWord,
final String originallyTypedWord, final boolean isBatchMode,
final String separatorString) {
+ // TODO: Prioritize adding a unit test for this method (as it is especially complex)
+ // TODO: Update the UserRecording LogBuffer as well as the MainLogBuffer
final ResearchLogger researchLogger = getInstance();
- // TODO: Verify that mCurrentLogUnit has been restored and contains the reverted word.
- final LogUnit logUnit;
- logUnit = researchLogger.mMainLogBuffer.peekLastLogUnit();
- if (originallyTypedWord.length() > 0 && hasLetters(originallyTypedWord)) {
- if (logUnit != null) {
- logUnit.setWords(originallyTypedWord);
- }
- }
- researchLogger.enqueueEvent(logUnit != null ? logUnit : researchLogger.mCurrentLogUnit,
- LOGSTATEMENT_LATINIME_REVERTCOMMIT, committedWord, originallyTypedWord,
- separatorString);
- if (logUnit != null) {
- logUnit.setContainsCorrection();
+ //
+ // 1. Remove separator LogUnit
+ final LogUnit lastLogUnit = researchLogger.mMainLogBuffer.peekLastLogUnit();
+ // Check that we're not at the beginning of input
+ if (lastLogUnit == null) return;
+ // Check that we're after a separator
+ if (lastLogUnit.getWordsAsString() != null) return;
+ // Remove separator
+ final LogUnit separatorLogUnit = researchLogger.mMainLogBuffer.unshiftIn();
+
+ // 2. Add revert LogStatement
+ final LogUnit revertedLogUnit = researchLogger.mMainLogBuffer.peekLastLogUnit();
+ if (revertedLogUnit == null) return;
+ if (!revertedLogUnit.getWordsAsString().equals(scrubDigitsFromString(committedWord))) {
+ // Any word associated with the reverted LogUnit has already had its digits scrubbed, so
+ // any digits in the committedWord argument must also be scrubbed for an accurate
+ // comparison.
+ return;
}
+ researchLogger.enqueueEvent(revertedLogUnit, LOGSTATEMENT_LATINIME_REVERTCOMMIT,
+ committedWord, originallyTypedWord, separatorString);
+
+ // 3. Update the word associated with the LogUnit
+ revertedLogUnit.setWords(originallyTypedWord);
+ revertedLogUnit.setContainsUserDeletions();
+
+ // 4. Re-add the separator LogUnit
+ researchLogger.mMainLogBuffer.shiftIn(separatorLogUnit);
+
+ // 5. Record stats
researchLogger.mStatistics.recordRevertCommit(SystemClock.uptimeMillis());
- researchLogger.commitCurrentLogUnitAsWord(originallyTypedWord, Long.MAX_VALUE, isBatchMode);
}
/**
@@ -1528,7 +1608,12 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_REVERTDOUBLESPACEPERIOD =
new LogStatement("RichInputConnectionRevertDoubleSpacePeriod", false, false);
public static void richInputConnection_revertDoubleSpacePeriod() {
- getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_REVERTDOUBLESPACEPERIOD);
+ final ResearchLogger researchLogger = getInstance();
+ // An extra LogUnit is added for the period; this is removed here because of the revert.
+ researchLogger.uncommitCurrentLogUnit(null, true /* dumpCurrentLogUnit */);
+ // TODO: This will probably be lost as the user backspaces further. Figure out how to put
+ // it into the right logUnit.
+ researchLogger.enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_REVERTDOUBLESPACEPERIOD);
}
/**
@@ -1571,25 +1656,6 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
}
private boolean isExpectingCommitText = false;
- /**
- * Log a call to (UnknownClass).commitPartialText
- *
- * SystemResponse: The IME is committing part of a word. This happens if a space is
- * automatically inserted to split a single typed string into two or more words.
- */
- // TODO: This method is currently unused. Find where it should be called from in the IME and
- // add invocations.
- private static final LogStatement LOGSTATEMENT_COMMIT_PARTIAL_TEXT =
- new LogStatement("CommitPartialText", true, false, "newCursorPosition");
- public static void commitPartialText(final String committedWord,
- final long lastTimestampOfWordData, final boolean isBatchMode) {
- final ResearchLogger researchLogger = getInstance();
- final String scrubbedWord = scrubDigitsFromString(committedWord);
- researchLogger.enqueueEvent(LOGSTATEMENT_COMMIT_PARTIAL_TEXT);
- researchLogger.mStatistics.recordAutoCorrection(SystemClock.uptimeMillis());
- researchLogger.commitCurrentLogUnitAsWord(scrubbedWord, lastTimestampOfWordData,
- isBatchMode);
- }
/**
* Log a call to RichInputConnection.commitText().
@@ -1613,12 +1679,24 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
}
/**
- * Shared event for logging committed text.
+ * Shared events for logging committed text.
+ *
+ * The "CommitTextEventHappened" LogStatement is written to the log even if privacy rules
+ * indicate that the word contents should not be logged. It has no contents, and only serves to
+ * record the event and thereby make it easier to calculate word-level statistics even when the
+ * word contents are unknown.
*/
private static final LogStatement LOGSTATEMENT_COMMITTEXT =
- new LogStatement("CommitText", true, false, "committedText", "isBatchMode");
+ new LogStatement("CommitText", true /* isPotentiallyPrivate */,
+ false /* isPotentiallyRevealing */, "committedText", "isBatchMode");
+ private static final LogStatement LOGSTATEMENT_COMMITTEXT_EVENT_HAPPENED =
+ new LogStatement("CommitTextEventHappened", false /* isPotentiallyPrivate */,
+ false /* isPotentiallyRevealing */);
private void enqueueCommitText(final String word, final boolean isBatchMode) {
+ // Event containing the word; will be published only if privacy checks pass
enqueueEvent(LOGSTATEMENT_COMMITTEXT, word, isBatchMode);
+ // Event not containing the word; will always be published
+ enqueueEvent(LOGSTATEMENT_COMMITTEXT_EVENT_HAPPENED);
}
/**
@@ -1766,17 +1844,26 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
SystemClock.uptimeMillis());
}
+ private static final LogStatement LOGSTATEMENT_LATINIME_HANDLEBACKSPACE =
+ new LogStatement("LatinIMEHandleBackspace", true, false, "numCharacters");
/**
* Log a call to LatinIME.handleBackspace() that is not a batch delete.
*
* UserInput: The user is deleting one or more characters by hitting the backspace key once.
* The covers single character deletes as well as deleting selections.
+ *
+ * @param numCharacters how many characters the backspace operation deleted
+ * @param shouldUncommitLogUnit whether to uncommit the last {@code LogUnit} in the
+ * {@code LogBuffer}
*/
- private static final LogStatement LOGSTATEMENT_LATINIME_HANDLEBACKSPACE =
- new LogStatement("LatinIMEHandleBackspace", true, false, "numCharacters");
- public static void latinIME_handleBackspace(final int numCharacters) {
+ public static void latinIME_handleBackspace(final int numCharacters,
+ final boolean shouldUncommitLogUnit) {
final ResearchLogger researchLogger = getInstance();
researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_HANDLEBACKSPACE, numCharacters);
+ if (shouldUncommitLogUnit) {
+ ResearchLogger.getInstance().uncommitCurrentLogUnit(
+ null, true /* dumpCurrentLogUnit */);
+ }
}
/**
@@ -1794,6 +1881,8 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
numCharacters);
researchLogger.mStatistics.recordGestureDelete(deletedText.length(),
SystemClock.uptimeMillis());
+ researchLogger.uncommitCurrentLogUnit(deletedText.toString(),
+ false /* dumpCurrentLogUnit */);
}
/**
@@ -1837,6 +1926,20 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
}
/**
+ * Call this method when the logging system has attempted publication of an n-gram.
+ *
+ * Statistics are gathered about the success or failure.
+ *
+ * @param publishabilityResultCode a result code as defined by
+ * {@code MainLogBuffer.PUBLISHABILITY_*}
+ */
+ static void recordPublishabilityResultCode(final int publishabilityResultCode) {
+ final ResearchLogger researchLogger = getInstance();
+ final Statistics statistics = researchLogger.mStatistics;
+ statistics.recordPublishabilityResultCode(publishabilityResultCode);
+ }
+
+ /**
* Log statistics.
*
* ContextualData, recorded at the end of a session.
@@ -1848,7 +1951,11 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
"averageTimeDuringRepeatedDelete", "averageTimeAfterDelete",
"dictionaryWordCount", "splitWordsCount", "gestureInputCount",
"gestureCharsCount", "gesturesDeletedCount", "manualSuggestionsCount",
- "revertCommitsCount", "correctedWordsCount", "autoCorrectionsCount");
+ "revertCommitsCount", "correctedWordsCount", "autoCorrectionsCount",
+ "publishableCount", "unpublishableStoppingCount",
+ "unpublishableIncorrectWordCount", "unpublishableSampledTooRecentlyCount",
+ "unpublishableDictionaryUnavailableCount", "unpublishableMayContainDigitCount",
+ "unpublishableNotInDictionaryCount");
private static void logStatistics() {
final ResearchLogger researchLogger = getInstance();
final Statistics statistics = researchLogger.mStatistics;
@@ -1863,6 +1970,10 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
statistics.mGesturesInputCount, statistics.mGesturesCharsCount,
statistics.mGesturesDeletedCount, statistics.mManualSuggestionsCount,
statistics.mRevertCommitsCount, statistics.mCorrectedWordsCount,
- statistics.mAutoCorrectionsCount);
+ statistics.mAutoCorrectionsCount, statistics.mPublishableCount,
+ statistics.mUnpublishableStoppingCount, statistics.mUnpublishableIncorrectWordCount,
+ statistics.mUnpublishableSampledTooRecently,
+ statistics.mUnpublishableDictionaryUnavailable,
+ statistics.mUnpublishableMayContainDigit, statistics.mUnpublishableNotInDictionary);
}
}
diff --git a/java/src/com/android/inputmethod/research/Statistics.java b/java/src/com/android/inputmethod/research/Statistics.java
index 7f6c851bb..e573ca012 100644
--- a/java/src/com/android/inputmethod/research/Statistics.java
+++ b/java/src/com/android/inputmethod/research/Statistics.java
@@ -61,6 +61,16 @@ public class Statistics {
boolean mIsEmptyUponStarting;
boolean mIsEmptinessStateKnown;
+ // Counts of how often an n-gram is collected or not, and the reasons for the decision.
+ // Keep consistent with publishability result code list in MainLogBuffer
+ int mPublishableCount;
+ int mUnpublishableStoppingCount;
+ int mUnpublishableIncorrectWordCount;
+ int mUnpublishableSampledTooRecently;
+ int mUnpublishableDictionaryUnavailable;
+ int mUnpublishableMayContainDigit;
+ int mUnpublishableNotInDictionary;
+
// Timers to count average time to enter a key, first press a delete key,
// between delete keys, and then to return typing after a delete key.
final AverageTimeCounter mKeyCounter = new AverageTimeCounter();
@@ -133,6 +143,13 @@ public class Statistics {
mAfterDeleteKeyCounter.reset();
mGesturesCharsCount = 0;
mGesturesDeletedCount = 0;
+ mPublishableCount = 0;
+ mUnpublishableStoppingCount = 0;
+ mUnpublishableIncorrectWordCount = 0;
+ mUnpublishableSampledTooRecently = 0;
+ mUnpublishableDictionaryUnavailable = 0;
+ mUnpublishableMayContainDigit = 0;
+ mUnpublishableNotInDictionary = 0;
mLastTapTime = 0;
mIsLastKeyDeleteKey = false;
@@ -230,4 +247,31 @@ public class Statistics {
mIsLastKeyDeleteKey = isDeletion;
mLastTapTime = time;
}
+
+ public void recordPublishabilityResultCode(final int publishabilityResultCode) {
+ // Keep consistent with publishability result code list in MainLogBuffer
+ switch (publishabilityResultCode) {
+ case MainLogBuffer.PUBLISHABILITY_PUBLISHABLE:
+ mPublishableCount++;
+ break;
+ case MainLogBuffer.PUBLISHABILITY_UNPUBLISHABLE_STOPPING:
+ mUnpublishableStoppingCount++;
+ break;
+ case MainLogBuffer.PUBLISHABILITY_UNPUBLISHABLE_INCORRECT_WORD_COUNT:
+ mUnpublishableIncorrectWordCount++;
+ break;
+ case MainLogBuffer.PUBLISHABILITY_UNPUBLISHABLE_SAMPLED_TOO_RECENTLY:
+ mUnpublishableSampledTooRecently++;
+ break;
+ case MainLogBuffer.PUBLISHABILITY_UNPUBLISHABLE_DICTIONARY_UNAVAILABLE:
+ mUnpublishableDictionaryUnavailable++;
+ break;
+ case MainLogBuffer.PUBLISHABILITY_UNPUBLISHABLE_MAY_CONTAIN_DIGIT:
+ mUnpublishableMayContainDigit++;
+ break;
+ case MainLogBuffer.PUBLISHABILITY_UNPUBLISHABLE_NOT_IN_DICTIONARY:
+ mUnpublishableNotInDictionary++;
+ break;
+ }
+ }
}
diff --git a/java/src/com/android/inputmethod/research/Uploader.java b/java/src/com/android/inputmethod/research/Uploader.java
index ba05ec12b..c7ea3e69d 100644
--- a/java/src/com/android/inputmethod/research/Uploader.java
+++ b/java/src/com/android/inputmethod/research/Uploader.java
@@ -49,7 +49,7 @@ public final class Uploader {
private static final boolean DEBUG = false
&& ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS_DEBUG;
// Set IS_INHIBITING_AUTO_UPLOAD to true for local testing
- private static final boolean IS_INHIBITING_AUTO_UPLOAD = false
+ private static final boolean IS_INHIBITING_UPLOAD = false
&& ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS_DEBUG;
private static final int BUF_SIZE = 1024 * 8;
@@ -76,7 +76,7 @@ public final class Uploader {
}
public boolean isPossibleToUpload() {
- return hasUploadingPermission() && mUrl != null && !IS_INHIBITING_AUTO_UPLOAD;
+ return hasUploadingPermission() && mUrl != null && !IS_INHIBITING_UPLOAD;
}
private boolean hasUploadingPermission() {