aboutsummaryrefslogtreecommitdiffstats
path: root/java/src
diff options
context:
space:
mode:
Diffstat (limited to 'java/src')
-rw-r--r--java/src/com/android/inputmethod/compat/TextViewCompatUtils.java18
-rw-r--r--java/src/com/android/inputmethod/compat/UserDictionaryCompatUtils.java10
-rw-r--r--java/src/com/android/inputmethod/dictionarypack/ActionBatch.java26
-rw-r--r--java/src/com/android/inputmethod/dictionarypack/ButtonSwitcher.java155
-rw-r--r--java/src/com/android/inputmethod/dictionarypack/DictionaryDownloadProgressBar.java178
-rw-r--r--java/src/com/android/inputmethod/dictionarypack/DictionaryListInterfaceState.java67
-rw-r--r--java/src/com/android/inputmethod/dictionarypack/DictionaryProvider.java16
-rw-r--r--java/src/com/android/inputmethod/dictionarypack/DictionaryService.java5
-rw-r--r--java/src/com/android/inputmethod/dictionarypack/DictionarySettingsFragment.java34
-rw-r--r--java/src/com/android/inputmethod/dictionarypack/LogProblemReporter.java3
-rw-r--r--java/src/com/android/inputmethod/dictionarypack/MetadataDbHelper.java92
-rw-r--r--java/src/com/android/inputmethod/dictionarypack/MetadataUriGetter.java33
-rw-r--r--java/src/com/android/inputmethod/dictionarypack/PrivateLog.java23
-rw-r--r--java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java29
-rw-r--r--java/src/com/android/inputmethod/dictionarypack/WordListPreference.java133
-rw-r--r--java/src/com/android/inputmethod/keyboard/Key.java2
-rw-r--r--java/src/com/android/inputmethod/keyboard/KeyboardActionListener.java14
-rw-r--r--java/src/com/android/inputmethod/keyboard/KeyboardId.java23
-rw-r--r--java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java56
-rw-r--r--java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java17
-rw-r--r--java/src/com/android/inputmethod/keyboard/KeyboardView.java2
-rw-r--r--java/src/com/android/inputmethod/keyboard/MainKeyboardView.java52
-rw-r--r--java/src/com/android/inputmethod/keyboard/PointerTracker.java117
-rw-r--r--java/src/com/android/inputmethod/keyboard/ProximityInfo.java17
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/GestureStroke.java7
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/GestureStrokeWithPreviewPoints.java136
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/GestureTrail.java (renamed from java/src/com/android/inputmethod/keyboard/internal/GesturePreviewTrail.java)67
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/GestureTrailsPreview.java62
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/KeyboardState.java73
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueue.java62
-rw-r--r--java/src/com/android/inputmethod/latin/BinaryDictionary.java24
-rw-r--r--java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java31
-rw-r--r--java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java23
-rw-r--r--java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java36
-rw-r--r--java/src/com/android/inputmethod/latin/DebugSettings.java14
-rw-r--r--java/src/com/android/inputmethod/latin/Dictionary.java9
-rw-r--r--java/src/com/android/inputmethod/latin/DictionaryCollection.java7
-rw-r--r--java/src/com/android/inputmethod/latin/DictionaryInfoUtils.java7
-rw-r--r--java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java6
-rw-r--r--java/src/com/android/inputmethod/latin/ExpandableDictionary.java3
-rw-r--r--java/src/com/android/inputmethod/latin/FeedbackUtils.java9
-rw-r--r--java/src/com/android/inputmethod/latin/JniUtils.java14
-rw-r--r--java/src/com/android/inputmethod/latin/LatinIME.java64
-rw-r--r--java/src/com/android/inputmethod/latin/MetadataFileUriGetter.java12
-rw-r--r--java/src/com/android/inputmethod/latin/ResourceUtils.java135
-rw-r--r--java/src/com/android/inputmethod/latin/RichInputConnection.java5
-rw-r--r--java/src/com/android/inputmethod/latin/RichInputMethodManager.java140
-rw-r--r--java/src/com/android/inputmethod/latin/SeekBarDialogPreference.java2
-rw-r--r--java/src/com/android/inputmethod/latin/Settings.java14
-rw-r--r--java/src/com/android/inputmethod/latin/SettingsFragment.java55
-rw-r--r--java/src/com/android/inputmethod/latin/SettingsValues.java15
-rw-r--r--java/src/com/android/inputmethod/latin/StringUtils.java17
-rw-r--r--java/src/com/android/inputmethod/latin/SubtypeSwitcher.java12
-rw-r--r--java/src/com/android/inputmethod/latin/Suggest.java17
-rw-r--r--java/src/com/android/inputmethod/latin/SuggestedWords.java6
-rw-r--r--java/src/com/android/inputmethod/latin/SynchronouslyLoadedContactsBinaryDictionary.java5
-rw-r--r--java/src/com/android/inputmethod/latin/SynchronouslyLoadedUserBinaryDictionary.java5
-rw-r--r--java/src/com/android/inputmethod/latin/Utils.java15
-rw-r--r--java/src/com/android/inputmethod/latin/WordComposer.java4
-rw-r--r--java/src/com/android/inputmethod/latin/personalization/AccountUtils.java47
-rw-r--r--java/src/com/android/inputmethod/latin/setup/SetupActivity.java296
-rw-r--r--java/src/com/android/inputmethod/latin/setup/SetupStartIndicatorView.java123
-rw-r--r--java/src/com/android/inputmethod/latin/setup/SetupStepIndicatorView.java10
-rw-r--r--java/src/com/android/inputmethod/latin/setup/SetupWizardActivity.java493
-rw-r--r--java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java38
-rw-r--r--java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java28
-rw-r--r--java/src/com/android/inputmethod/latin/spellcheck/DictAndKeyboard.java56
-rw-r--r--java/src/com/android/inputmethod/latin/spellcheck/DictAndProximity.java32
-rw-r--r--java/src/com/android/inputmethod/latin/spellcheck/DictionaryPool.java25
-rw-r--r--java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerProximityInfo.java462
-rw-r--r--java/src/com/android/inputmethod/latin/suggestions/MoreSuggestions.java34
-rw-r--r--java/src/com/android/inputmethod/latin/suggestions/MoreSuggestionsView.java25
-rw-r--r--java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java12
-rw-r--r--java/src/com/android/inputmethod/latin/userdictionary/UserDictionaryAddWordContents.java261
-rw-r--r--java/src/com/android/inputmethod/latin/userdictionary/UserDictionaryAddWordFragment.java162
-rw-r--r--java/src/com/android/inputmethod/latin/userdictionary/UserDictionaryList.java127
-rw-r--r--java/src/com/android/inputmethod/latin/userdictionary/UserDictionaryLocalePicker.java36
-rw-r--r--java/src/com/android/inputmethod/latin/userdictionary/UserDictionarySettings.java334
-rw-r--r--java/src/com/android/inputmethod/research/FeedbackFragment.java39
-rw-r--r--java/src/com/android/inputmethod/research/FixedLogBuffer.java72
-rw-r--r--java/src/com/android/inputmethod/research/LogUnit.java102
-rw-r--r--java/src/com/android/inputmethod/research/MainLogBuffer.java89
-rw-r--r--java/src/com/android/inputmethod/research/ResearchLog.java86
-rw-r--r--java/src/com/android/inputmethod/research/ResearchLogDirectory.java14
-rw-r--r--java/src/com/android/inputmethod/research/ResearchLogger.java235
-rw-r--r--java/src/com/android/inputmethod/research/UploaderService.java25
86 files changed, 3703 insertions, 1795 deletions
diff --git a/java/src/com/android/inputmethod/compat/TextViewCompatUtils.java b/java/src/com/android/inputmethod/compat/TextViewCompatUtils.java
index d4f1ea830..f8e1902c0 100644
--- a/java/src/com/android/inputmethod/compat/TextViewCompatUtils.java
+++ b/java/src/com/android/inputmethod/compat/TextViewCompatUtils.java
@@ -22,23 +22,23 @@ import android.widget.TextView;
import java.lang.reflect.Method;
public final class TextViewCompatUtils {
- // Note that TextView.setCompoundDrawablesRelative(Drawable,Drawable,Drawable,Drawable) has
- // been introduced in API level 17 (Build.VERSION_CODE.JELLY_BEAN_MR1).
- private static final Method METHOD_setCompoundDrawablesRelative = CompatUtils.getMethod(
- TextView.class, "setCompoundDrawablesRelative",
+ // Note that TextView.setCompoundDrawablesRelativeWithIntrinsicBounds(Drawable,Drawable,
+ // Drawable,Drawable) has been introduced in API level 17 (Build.VERSION_CODE.JELLY_BEAN_MR1).
+ private static final Method METHOD_setCompoundDrawablesRelativeWithIntrinsicBounds =
+ CompatUtils.getMethod(TextView.class, "setCompoundDrawablesRelativeWithIntrinsicBounds",
Drawable.class, Drawable.class, Drawable.class, Drawable.class);
private TextViewCompatUtils() {
// This utility class is not publicly instantiable.
}
- public static void setCompoundDrawablesRelative(final TextView textView, final Drawable start,
- final Drawable top, final Drawable end, final Drawable bottom) {
- if (METHOD_setCompoundDrawablesRelative == null) {
- textView.setCompoundDrawables(start, top, end, bottom);
+ public static void setCompoundDrawablesRelativeWithIntrinsicBounds(final TextView textView,
+ final Drawable start, final Drawable top, final Drawable end, final Drawable bottom) {
+ if (METHOD_setCompoundDrawablesRelativeWithIntrinsicBounds == null) {
+ textView.setCompoundDrawablesWithIntrinsicBounds(start, top, end, bottom);
return;
}
- CompatUtils.invoke(textView, null, METHOD_setCompoundDrawablesRelative,
+ CompatUtils.invoke(textView, null, METHOD_setCompoundDrawablesRelativeWithIntrinsicBounds,
start, top, end, bottom);
}
}
diff --git a/java/src/com/android/inputmethod/compat/UserDictionaryCompatUtils.java b/java/src/com/android/inputmethod/compat/UserDictionaryCompatUtils.java
index ff6561c58..a0d76415c 100644
--- a/java/src/com/android/inputmethod/compat/UserDictionaryCompatUtils.java
+++ b/java/src/com/android/inputmethod/compat/UserDictionaryCompatUtils.java
@@ -28,6 +28,7 @@ public final class UserDictionaryCompatUtils {
private static final Method METHOD_addWord = CompatUtils.getMethod(Words.class, "addWord",
Context.class, String.class, Integer.TYPE, String.class, Locale.class);
+ @SuppressWarnings("deprecation")
public static void addWord(final Context context, final String word, final int freq,
final String shortcut, final Locale locale) {
if (hasNewerAddWord()) {
@@ -39,13 +40,18 @@ public final class UserDictionaryCompatUtils {
if (null == locale) {
localeType = Words.LOCALE_TYPE_ALL;
} else {
- localeType = Words.LOCALE_TYPE_CURRENT;
+ final Locale currentLocale = context.getResources().getConfiguration().locale;
+ if (locale.equals(currentLocale)) {
+ localeType = Words.LOCALE_TYPE_CURRENT;
+ } else {
+ localeType = Words.LOCALE_TYPE_ALL;
+ }
}
Words.addWord(context, word, freq, localeType);
}
}
- private static final boolean hasNewerAddWord() {
+ public static final boolean hasNewerAddWord() {
return null != METHOD_addWord;
}
}
diff --git a/java/src/com/android/inputmethod/dictionarypack/ActionBatch.java b/java/src/com/android/inputmethod/dictionarypack/ActionBatch.java
index df4a52f4e..bf2230553 100644
--- a/java/src/com/android/inputmethod/dictionarypack/ActionBatch.java
+++ b/java/src/com/android/inputmethod/dictionarypack/ActionBatch.java
@@ -138,7 +138,12 @@ public final class ActionBatch {
if (null == manager) return;
// This is an upgraded word list: we should download it.
- final Uri uri = Uri.parse(mWordList.mRemoteFilename);
+ // Adding a disambiguator to circumvent a bug in older versions of DownloadManager.
+ // DownloadManager also stupidly cuts the extension to replace with its own that it
+ // gets from the content-type. We need to circumvent this.
+ final String disambiguator = "#" + System.currentTimeMillis()
+ + com.android.inputmethod.latin.Utils.getVersionName(context) + ".dict";
+ final Uri uri = Uri.parse(mWordList.mRemoteFilename + disambiguator);
final Request request = new Request(uri);
final Resources res = context.getResources();
@@ -174,7 +179,7 @@ public final class ActionBatch {
final long downloadId = UpdateHandler.registerDownloadRequest(manager, request, db,
mWordList.mId, mWordList.mVersion);
Utils.l("Starting download of", uri, "with id", downloadId);
- PrivateLog.log("Starting download of " + uri + ", id : " + downloadId, context);
+ PrivateLog.log("Starting download of " + uri + ", id : " + downloadId);
}
}
@@ -333,7 +338,7 @@ public final class ActionBatch {
mWordList.mRemoteFilename, mWordList.mLastUpdate, mWordList.mChecksum,
mWordList.mFileSize, mWordList.mVersion, mWordList.mFormatVersion);
PrivateLog.log("Insert 'available' record for " + mWordList.mDescription
- + " and locale " + mWordList.mLocale, context);
+ + " and locale " + mWordList.mLocale);
db.insert(MetadataDbHelper.METADATA_TABLE_NAME, null, values);
}
}
@@ -383,7 +388,7 @@ public final class ActionBatch {
mWordList.mChecksum, mWordList.mFileSize, mWordList.mVersion,
mWordList.mFormatVersion);
PrivateLog.log("Insert 'preinstalled' record for " + mWordList.mDescription
- + " and locale " + mWordList.mLocale, context);
+ + " and locale " + mWordList.mLocale);
db.insert(MetadataDbHelper.METADATA_TABLE_NAME, null, values);
}
}
@@ -424,7 +429,7 @@ public final class ActionBatch {
mWordList.mRemoteFilename, mWordList.mLastUpdate, mWordList.mChecksum,
mWordList.mFileSize, mWordList.mVersion, mWordList.mFormatVersion);
PrivateLog.log("Updating record for " + mWordList.mDescription
- + " and locale " + mWordList.mLocale, context);
+ + " and locale " + mWordList.mLocale);
db.update(MetadataDbHelper.METADATA_TABLE_NAME, values,
MetadataDbHelper.WORDLISTID_COLUMN + " = ? AND "
+ MetadataDbHelper.VERSION_COLUMN + " = ?",
@@ -478,13 +483,14 @@ public final class ActionBatch {
if (MetadataDbHelper.STATUS_INSTALLED == status
|| MetadataDbHelper.STATUS_DISABLED == status
|| MetadataDbHelper.STATUS_DELETING == status) {
- // If it is installed or disabled, then we cannot remove the entry lest the user
- // lose the ability to delete the file or otherwise administrate it. We will thus
- // leave it as is, but remove the URI from the database since it is not supposed to
- // be accessible any more.
+ // If it is installed or disabled, we need to mark it as deleted so that LatinIME
+ // will remove it next time it enquires for dictionaries.
// If it is deleting and we don't have a new version, then we have to wait until
- // Android Keyboard actually has deleted it before we can remove its metadata.
+ // LatinIME actually has deleted it before we can remove its metadata.
+ // In both cases, remove the URI from the database since it is not supposed to
+ // be accessible any more.
values.put(MetadataDbHelper.REMOTE_FILENAME_COLUMN, "");
+ values.put(MetadataDbHelper.STATUS_COLUMN, MetadataDbHelper.STATUS_DELETING);
db.update(MetadataDbHelper.METADATA_TABLE_NAME, values,
MetadataDbHelper.WORDLISTID_COLUMN + " = ? AND "
+ MetadataDbHelper.VERSION_COLUMN + " = ?",
diff --git a/java/src/com/android/inputmethod/dictionarypack/ButtonSwitcher.java b/java/src/com/android/inputmethod/dictionarypack/ButtonSwitcher.java
new file mode 100644
index 000000000..5ab94a429
--- /dev/null
+++ b/java/src/com/android/inputmethod/dictionarypack/ButtonSwitcher.java
@@ -0,0 +1,155 @@
+/**
+ * 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.dictionarypack;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewPropertyAnimator;
+import android.widget.Button;
+import android.widget.FrameLayout;
+
+import com.android.inputmethod.latin.R;
+
+/**
+ * A view that handles buttons inside it according to a status.
+ */
+public class ButtonSwitcher extends FrameLayout {
+ public static final int NOT_INITIALIZED = -1;
+ public static final int STATUS_NO_BUTTON = 0;
+ public static final int STATUS_INSTALL = 1;
+ public static final int STATUS_CANCEL = 2;
+ public static final int STATUS_DELETE = 3;
+ // One of the above
+ private int mStatus = NOT_INITIALIZED;
+ private int mAnimateToStatus = NOT_INITIALIZED;
+
+ // Animation directions
+ public static final int ANIMATION_IN = 1;
+ public static final int ANIMATION_OUT = 2;
+
+ private Button mInstallButton;
+ private Button mCancelButton;
+ private Button mDeleteButton;
+ private OnClickListener mOnClickListener;
+
+ public ButtonSwitcher(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public ButtonSwitcher(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ @Override
+ protected void onLayout(final boolean changed, final int left, final int top, final int right,
+ final int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+ 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);
+ setButtonPositionWithoutAnimation(mStatus);
+ if (mAnimateToStatus != NOT_INITIALIZED) {
+ // We have been asked to animate before we were ready, so we took a note of it.
+ // We are now ready: launch the animation.
+ animateButtonPosition(mStatus, mAnimateToStatus);
+ mStatus = mAnimateToStatus;
+ mAnimateToStatus = NOT_INITIALIZED;
+ }
+ }
+
+ private Button getButton(final int status) {
+ switch(status) {
+ case STATUS_INSTALL:
+ return mInstallButton;
+ case STATUS_CANCEL:
+ return mCancelButton;
+ case STATUS_DELETE:
+ return mDeleteButton;
+ default:
+ return null;
+ }
+ }
+
+ public void setStatusAndUpdateVisuals(final int status) {
+ if (mStatus == NOT_INITIALIZED) {
+ setButtonPositionWithoutAnimation(status);
+ mStatus = status;
+ } else {
+ if (null == mInstallButton) {
+ // We may come here before we have been layout. In this case we don't know our
+ // size yet so we can't start animations so we need to remember what animation to
+ // start once layout has gone through.
+ mAnimateToStatus = status;
+ } else {
+ animateButtonPosition(mStatus, status);
+ mStatus = status;
+ }
+ }
+ }
+
+ private void setButtonPositionWithoutAnimation(final int status) {
+ // This may be called by setStatus() before the layout has come yet.
+ if (null == mInstallButton) return;
+ final int width = getWidth();
+ // Set to out of the screen if that's not the currently displayed status
+ mInstallButton.setTranslationX(STATUS_INSTALL == status ? 0 : width);
+ mCancelButton.setTranslationX(STATUS_CANCEL == status ? 0 : width);
+ mDeleteButton.setTranslationX(STATUS_DELETE == status ? 0 : width);
+ }
+
+ private void animateButtonPosition(final int oldStatus, final int newStatus) {
+ final View oldButton = getButton(oldStatus);
+ final View newButton = getButton(newStatus);
+ if (null != oldButton && null != newButton) {
+ // Transition between two buttons : animate out, then in
+ animateButton(oldButton, ANIMATION_OUT).setListener(
+ new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(final Animator animation) {
+ if (newStatus != mStatus) return;
+ animateButton(newButton, ANIMATION_IN);
+ }
+ });
+ } else if (null != oldButton) {
+ animateButton(oldButton, ANIMATION_OUT);
+ } else if (null != newButton) {
+ animateButton(newButton, ANIMATION_IN);
+ }
+ }
+
+ public void setInternalOnClickListener(final OnClickListener listener) {
+ mOnClickListener = listener;
+ }
+
+ private ViewPropertyAnimator animateButton(final View button, final int direction) {
+ final float outerX = getWidth();
+ final float innerX = button.getX() - button.getTranslationX();
+ if (ANIMATION_IN == direction) {
+ button.setClickable(true);
+ return button.animate().translationX(0);
+ } else {
+ button.setClickable(false);
+ return button.animate().translationX(outerX - innerX);
+ }
+ }
+}
diff --git a/java/src/com/android/inputmethod/dictionarypack/DictionaryDownloadProgressBar.java b/java/src/com/android/inputmethod/dictionarypack/DictionaryDownloadProgressBar.java
new file mode 100644
index 000000000..88b5032e3
--- /dev/null
+++ b/java/src/com/android/inputmethod/dictionarypack/DictionaryDownloadProgressBar.java
@@ -0,0 +1,178 @@
+/**
+ * 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.dictionarypack;
+
+import android.app.DownloadManager;
+import android.app.DownloadManager.Query;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.os.Handler;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.View;
+import android.widget.ProgressBar;
+
+public class DictionaryDownloadProgressBar extends ProgressBar {
+ @SuppressWarnings("unused")
+ private static final String TAG = DictionaryDownloadProgressBar.class.getSimpleName();
+ private static final int NOT_A_DOWNLOADMANAGER_PENDING_ID = 0;
+
+ private String mClientId;
+ private String mWordlistId;
+ private boolean mIsCurrentlyAttachedToWindow = false;
+ private Thread mReporterThread = null;
+
+ public DictionaryDownloadProgressBar(final Context context) {
+ super(context);
+ }
+
+ public DictionaryDownloadProgressBar(final Context context, final AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public void setIds(final String clientId, final String wordlistId) {
+ mClientId = clientId;
+ mWordlistId = wordlistId;
+ }
+
+ static private int getDownloadManagerPendingIdFromWordlistId(final Context context,
+ final String clientId, final String wordlistId) {
+ final SQLiteDatabase db = MetadataDbHelper.getDb(context, clientId);
+ final ContentValues wordlistValues =
+ MetadataDbHelper.getContentValuesOfLatestAvailableWordlistById(db, wordlistId);
+ if (null == wordlistValues) {
+ // We don't know anything about a word list with this id. Bug? This should never
+ // happen, but still return to prevent a crash.
+ Log.e(TAG, "Unexpected word list ID: " + wordlistId);
+ return NOT_A_DOWNLOADMANAGER_PENDING_ID;
+ }
+ return wordlistValues.getAsInteger(MetadataDbHelper.PENDINGID_COLUMN);
+ }
+
+ /*
+ * This method will stop any running updater thread for this progress bar and create and run
+ * a new one only if the progress bar is visible.
+ * Hence, as a result of calling this method, the progress bar will have an updater thread
+ * running if and only if the progress bar is visible.
+ */
+ private void updateReporterThreadRunningStatusAccordingToVisibility() {
+ if (null != mReporterThread) mReporterThread.interrupt();
+ if (mIsCurrentlyAttachedToWindow && View.VISIBLE == getVisibility()) {
+ final int downloadManagerPendingId =
+ getDownloadManagerPendingIdFromWordlistId(getContext(), mClientId, mWordlistId);
+ if (NOT_A_DOWNLOADMANAGER_PENDING_ID == downloadManagerPendingId) {
+ // Can't get the ID. This is never supposed to happen, but still clear the updater
+ // thread and return to avoid a crash.
+ mReporterThread = null;
+ return;
+ }
+ final UpdaterThread updaterThread =
+ new UpdaterThread(getContext(), downloadManagerPendingId);
+ updaterThread.start();
+ mReporterThread = updaterThread;
+ } else {
+ // We're not going to restart the thread anyway, so we may as well garbage collect it.
+ mReporterThread = null;
+ }
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ mIsCurrentlyAttachedToWindow = true;
+ updateReporterThreadRunningStatusAccordingToVisibility();
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ mIsCurrentlyAttachedToWindow = false;
+ updateReporterThreadRunningStatusAccordingToVisibility();
+ }
+
+ private class UpdaterThread extends Thread {
+ private final static int REPORT_PERIOD = 150; // how often to report progress, in ms
+ final DownloadManager mDownloadManager;
+ final int mId;
+ public UpdaterThread(final Context context, final int id) {
+ super();
+ mDownloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
+ mId = id;
+ }
+ @Override
+ public void run() {
+ try {
+ // It's almost impossible that mDownloadManager is null (it would mean it has been
+ // disabled between pressing the 'install' button and displaying the progress
+ // bar), but just in case.
+ if (null == mDownloadManager) return;
+ final UpdateHelper updateHelper = new UpdateHelper();
+ final Query query = new Query().setFilterById(mId);
+ int lastProgress = 0;
+ setIndeterminate(true);
+ while (!isInterrupted()) {
+ final Cursor cursor = mDownloadManager.query(query);
+ if (null == cursor) {
+ // Can't contact DownloadManager: this should never happen.
+ return;
+ }
+ try {
+ if (cursor.moveToNext()) {
+ final int columnBytesDownloadedSoFar = cursor.getColumnIndex(
+ DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR);
+ final int bytesDownloadedSoFar =
+ cursor.getInt(columnBytesDownloadedSoFar);
+ updateHelper.setProgressFromAnotherThread(bytesDownloadedSoFar);
+ } else {
+ // Download has finished and DownloadManager has already been asked to
+ // clean up the db entry.
+ updateHelper.setProgressFromAnotherThread(getMax());
+ return;
+ }
+ } finally {
+ cursor.close();
+ }
+ Thread.sleep(REPORT_PERIOD);
+ }
+ } catch (InterruptedException e) {
+ // Do nothing and terminate normally.
+ }
+ }
+
+ private class UpdateHelper implements Runnable {
+ private int mProgress;
+ @Override
+ public void run() {
+ setIndeterminate(false);
+ setProgress(mProgress);
+ }
+ public void setProgressFromAnotherThread(final int progress) {
+ if (mProgress != progress) {
+ mProgress = progress;
+ // For some unknown reason, setProgress just does not work from a separate
+ // thread, although the code in ProgressBar looks like it should. Thus, we
+ // resort to a runnable posted to the handler of the view.
+ final Handler handler = getHandler();
+ // It's possible to come here before this view has been laid out. If so,
+ // just ignore the call - it will be updated again later.
+ if (null == handler) return;
+ handler.post(this);
+ }
+ }
+ }
+ }
+}
diff --git a/java/src/com/android/inputmethod/dictionarypack/DictionaryListInterfaceState.java b/java/src/com/android/inputmethod/dictionarypack/DictionaryListInterfaceState.java
new file mode 100644
index 000000000..de3711c27
--- /dev/null
+++ b/java/src/com/android/inputmethod/dictionarypack/DictionaryListInterfaceState.java
@@ -0,0 +1,67 @@
+/**
+ * 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.dictionarypack;
+
+import com.android.inputmethod.latin.CollectionUtils;
+
+import java.util.HashMap;
+
+/**
+ * Helper class to maintain the interface state of word list preferences.
+ *
+ * This is necessary because the views are created on-demand by calling code. There are many
+ * situations where views are renewed with little relation with user interaction. For example,
+ * when scrolling, the view is reused so it doesn't keep its state, which means we need to keep
+ * it separately. Also whenever the underlying dictionary list undergoes a change (for example,
+ * update the metadata, or finish downloading) the whole list has to be thrown out and recreated
+ * in case some dictionaries appeared, disappeared, changed states etc.
+ */
+public class DictionaryListInterfaceState {
+ private static class State {
+ public boolean mOpen = false;
+ public int mStatus = MetadataDbHelper.STATUS_UNKNOWN;
+ }
+
+ private HashMap<String, State> mWordlistToState = CollectionUtils.newHashMap();
+
+ public boolean isOpen(final String wordlistId) {
+ final State state = mWordlistToState.get(wordlistId);
+ if (null == state) return false;
+ return state.mOpen;
+ }
+
+ public int getStatus(final String wordlistId) {
+ final State state = mWordlistToState.get(wordlistId);
+ if (null == state) return MetadataDbHelper.STATUS_UNKNOWN;
+ return state.mStatus;
+ }
+
+ public void setOpen(final String wordlistId, final int status) {
+ final State newState;
+ final State state = mWordlistToState.get(wordlistId);
+ newState = null == state ? new State() : state;
+ newState.mOpen = true;
+ newState.mStatus = status;
+ mWordlistToState.put(wordlistId, newState);
+ }
+
+ public void closeAll() {
+ for (final State state : mWordlistToState.values()) {
+ state.mOpen = false;
+ }
+ }
+}
diff --git a/java/src/com/android/inputmethod/dictionarypack/DictionaryProvider.java b/java/src/com/android/inputmethod/dictionarypack/DictionaryProvider.java
index f8d1c4fc9..4fbe16233 100644
--- a/java/src/com/android/inputmethod/dictionarypack/DictionaryProvider.java
+++ b/java/src/com/android/inputmethod/dictionarypack/DictionaryProvider.java
@@ -189,7 +189,7 @@ public final class DictionaryProvider extends ContentProvider {
*/
@Override
public String getType(final Uri uri) {
- PrivateLog.log("Asked for type of : " + uri, this);
+ PrivateLog.log("Asked for type of : " + uri);
final int match = matchUri(uri);
switch (match) {
case NO_MATCH: return null;
@@ -220,7 +220,7 @@ public final class DictionaryProvider extends ContentProvider {
public Cursor query(final Uri uri, final String[] projection, final String selection,
final String[] selectionArgs, final String sortOrder) {
Utils.l("Uri =", uri);
- PrivateLog.log("Query : " + uri, this);
+ PrivateLog.log("Query : " + uri);
final String clientId = getClientId(uri);
final int match = matchUri(uri);
switch (match) {
@@ -228,7 +228,7 @@ public final class DictionaryProvider extends ContentProvider {
case DICTIONARY_V2_WHOLE_LIST:
final Cursor c = MetadataDbHelper.queryDictionaries(getContext(), clientId);
Utils.l("List of dictionaries with count", c.getCount());
- PrivateLog.log("Returned a list of " + c.getCount() + " items", this);
+ PrivateLog.log("Returned a list of " + c.getCount() + " items");
return c;
case DICTIONARY_V2_DICT_INFO:
// In protocol version 2, we return null if the client is unknown. Otherwise
@@ -248,10 +248,10 @@ public final class DictionaryProvider extends ContentProvider {
// TODO: pass clientId to the following function
DictionaryService.updateNowIfNotUpdatedInAVeryLongTime(getContext());
if (null != dictFiles && dictFiles.size() > 0) {
- PrivateLog.log("Returned " + dictFiles.size() + " files", this);
+ PrivateLog.log("Returned " + dictFiles.size() + " files");
return new ResourcePathCursor(dictFiles);
} else {
- PrivateLog.log("No dictionary files for this URL", this);
+ PrivateLog.log("No dictionary files for this URL");
return new ResourcePathCursor(Collections.<WordListInfo>emptyList());
}
// V2_METADATA and V2_DATAFILE are not supported for query()
@@ -488,7 +488,7 @@ public final class DictionaryProvider extends ContentProvider {
public Uri insert(final Uri uri, final ContentValues values)
throws UnsupportedOperationException {
if (null == uri || null == values) return null; // Should never happen but let's be safe
- PrivateLog.log("Insert, uri = " + uri.toString(), this);
+ PrivateLog.log("Insert, uri = " + uri.toString());
final String clientId = getClientId(uri);
switch (matchUri(uri)) {
case DICTIONARY_V2_METADATA:
@@ -517,7 +517,7 @@ public final class DictionaryProvider extends ContentProvider {
break;
case DICTIONARY_V1_WHOLE_LIST:
case DICTIONARY_V1_DICT_INFO:
- PrivateLog.log("Attempt to insert : " + uri, this);
+ PrivateLog.log("Attempt to insert : " + uri);
throw new UnsupportedOperationException(
"Insertion in the dictionary is not supported in this version");
}
@@ -532,7 +532,7 @@ public final class DictionaryProvider extends ContentProvider {
@Override
public int update(final Uri uri, final ContentValues values, final String selection,
final String[] selectionArgs) throws UnsupportedOperationException {
- PrivateLog.log("Attempt to update : " + uri, this);
+ PrivateLog.log("Attempt to update : " + uri);
throw new UnsupportedOperationException("Updating dictionary words is not supported");
}
}
diff --git a/java/src/com/android/inputmethod/dictionarypack/DictionaryService.java b/java/src/com/android/inputmethod/dictionarypack/DictionaryService.java
index 5817eb498..46bb5543a 100644
--- a/java/src/com/android/inputmethod/dictionarypack/DictionaryService.java
+++ b/java/src/com/android/inputmethod/dictionarypack/DictionaryService.java
@@ -21,7 +21,6 @@ import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
-import android.content.SharedPreferences;
import android.os.IBinder;
import android.text.format.DateUtils;
import android.util.Log;
@@ -190,7 +189,7 @@ public final class DictionaryService extends Service {
// is still more recent than UPDATE_FREQUENCY, do nothing.
if (!isLastUpdateAtLeastThisOld(context, UPDATE_FREQUENCY)) return;
- PrivateLog.log("Date changed - registering alarm", context);
+ PrivateLog.log("Date changed - registering alarm");
AlarmManager alarmManager = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
// Best effort to wake between midnight and MAX_ALARM_DELAY in the morning.
@@ -215,7 +214,7 @@ public final class DictionaryService extends Service {
private static boolean isLastUpdateAtLeastThisOld(final Context context, final long time) {
final long now = System.currentTimeMillis();
final long lastUpdate = MetadataDbHelper.getOldestUpdateTime(context);
- PrivateLog.log("Last update was " + lastUpdate, context);
+ PrivateLog.log("Last update was " + lastUpdate);
return lastUpdate + time < now;
}
diff --git a/java/src/com/android/inputmethod/dictionarypack/DictionarySettingsFragment.java b/java/src/com/android/inputmethod/dictionarypack/DictionarySettingsFragment.java
index 9e27c1f3f..618322357 100644
--- a/java/src/com/android/inputmethod/dictionarypack/DictionarySettingsFragment.java
+++ b/java/src/com/android/inputmethod/dictionarypack/DictionarySettingsFragment.java
@@ -64,6 +64,10 @@ public final class DictionarySettingsFragment extends PreferenceFragment
private ConnectivityManager mConnectivityManager;
private MenuItem mUpdateNowMenu;
private boolean mChangedSettings;
+ private DictionaryListInterfaceState mDictionaryListInterfaceState =
+ new DictionaryListInterfaceState();
+ private TreeMap<String, WordListPreference> mCurrentPreferenceMap =
+ new TreeMap<String, WordListPreference>(); // never null
private final BroadcastReceiver mConnectivityChangedReceiver = new BroadcastReceiver() {
@Override
@@ -276,13 +280,14 @@ public final class DictionarySettingsFragment extends PreferenceFragment
return result;
} else {
final String systemLocaleString = Locale.getDefault().toString();
- final TreeMap<String, WordListPreference> prefList =
+ final TreeMap<String, WordListPreference> prefMap =
new TreeMap<String, WordListPreference>();
final int idIndex = cursor.getColumnIndex(MetadataDbHelper.WORDLISTID_COLUMN);
final int versionIndex = cursor.getColumnIndex(MetadataDbHelper.VERSION_COLUMN);
final int localeIndex = cursor.getColumnIndex(MetadataDbHelper.LOCALE_COLUMN);
final int descriptionIndex = cursor.getColumnIndex(MetadataDbHelper.DESCRIPTION_COLUMN);
final int statusIndex = cursor.getColumnIndex(MetadataDbHelper.STATUS_COLUMN);
+ final int filesizeIndex = cursor.getColumnIndex(MetadataDbHelper.FILESIZE_COLUMN);
do {
final String wordlistId = cursor.getString(idIndex);
final int version = cursor.getInt(versionIndex);
@@ -292,18 +297,35 @@ public final class DictionarySettingsFragment extends PreferenceFragment
final int status = cursor.getInt(statusIndex);
final int matchLevel = LocaleUtils.getMatchLevel(systemLocaleString, localeString);
final String matchLevelString = LocaleUtils.getMatchLevelSortedString(matchLevel);
+ final int filesize = cursor.getInt(filesizeIndex);
// The key is sorted in lexicographic order, according to the match level, then
// the description.
final String key = matchLevelString + "." + description + "." + wordlistId;
- final WordListPreference existingPref = prefList.get(key);
+ final WordListPreference existingPref = prefMap.get(key);
if (null == existingPref || hasPriority(status, existingPref.mStatus)) {
- final WordListPreference pref = new WordListPreference(activity, mClientId,
- wordlistId, version, locale, description, status);
- prefList.put(key, pref);
+ final WordListPreference oldPreference = mCurrentPreferenceMap.get(key);
+ final WordListPreference pref;
+ if (null != oldPreference
+ && oldPreference.mVersion == version
+ && oldPreference.mLocale.equals(locale)) {
+ // If the old preference has all the new attributes, reuse it. We test
+ // for version and locale because although attributes other than status
+ // 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;
+ } else {
+ // Otherwise, discard it and create a new one instead.
+ pref = new WordListPreference(activity, mDictionaryListInterfaceState,
+ mClientId, wordlistId, version, locale, description, status,
+ filesize);
+ }
+ prefMap.put(key, pref);
}
} while (cursor.moveToNext());
cursor.close();
- return prefList.values();
+ mCurrentPreferenceMap = prefMap;
+ return prefMap.values();
}
}
diff --git a/java/src/com/android/inputmethod/dictionarypack/LogProblemReporter.java b/java/src/com/android/inputmethod/dictionarypack/LogProblemReporter.java
index c127ad540..c9e128d70 100644
--- a/java/src/com/android/inputmethod/dictionarypack/LogProblemReporter.java
+++ b/java/src/com/android/inputmethod/dictionarypack/LogProblemReporter.java
@@ -28,7 +28,8 @@ final class LogProblemReporter implements ProblemReporter {
TAG = tag;
}
+ @Override
public void report(final Exception e) {
- Log.e(TAG, "Reporting problem : " + e);
+ Log.e(TAG, "Reporting problem", e);
}
}
diff --git a/java/src/com/android/inputmethod/dictionarypack/MetadataDbHelper.java b/java/src/com/android/inputmethod/dictionarypack/MetadataDbHelper.java
index 55f545aad..03ed267c3 100644
--- a/java/src/com/android/inputmethod/dictionarypack/MetadataDbHelper.java
+++ b/java/src/com/android/inputmethod/dictionarypack/MetadataDbHelper.java
@@ -16,13 +16,11 @@
package com.android.inputmethod.dictionarypack;
-import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
-import android.provider.Settings;
import android.text.TextUtils;
import android.util.Log;
@@ -47,16 +45,16 @@ public class MetadataDbHelper extends SQLiteOpenHelper {
private static final int METADATA_DATABASE_INITIAL_VERSION = 3;
// This is the first released version of the database that implements CLIENTID. It is
// used to identify the versions for upgrades. This should never change going forward.
- private static final int METADATA_DATABASE_VERSION_WITH_CLIENTID = 5;
+ private static final int METADATA_DATABASE_VERSION_WITH_CLIENTID = 6;
// This is the current database version. It should be updated when the database schema
// gets updated. It is passed to the framework constructor of SQLiteOpenHelper, so
// that's what the framework uses to track our database version.
- private static final int METADATA_DATABASE_VERSION = 5;
+ private static final int METADATA_DATABASE_VERSION = 6;
private final static long NOT_A_DOWNLOAD_ID = -1;
public static final String METADATA_TABLE_NAME = "pendingUpdates";
- private static final String CLIENT_TABLE_NAME = "clients";
+ static final String CLIENT_TABLE_NAME = "clients";
public static final String PENDINGID_COLUMN = "pendingid"; // Download Manager ID
public static final String TYPE_COLUMN = "type";
public static final String STATUS_COLUMN = "status";
@@ -75,6 +73,7 @@ public class MetadataDbHelper extends SQLiteOpenHelper {
private static final String CLIENT_CLIENT_ID_COLUMN = "clientid";
private static final String CLIENT_METADATA_URI_COLUMN = "uri";
+ private static final String CLIENT_METADATA_ADDITIONAL_ID_COLUMN = "additionalid";
private static final String CLIENT_LAST_UPDATE_DATE_COLUMN = "lastupdate";
private static final String CLIENT_PENDINGID_COLUMN = "pendingid"; // Download Manager ID
@@ -130,6 +129,7 @@ public class MetadataDbHelper extends SQLiteOpenHelper {
"CREATE TABLE IF NOT EXISTS " + CLIENT_TABLE_NAME + " ("
+ CLIENT_CLIENT_ID_COLUMN + " TEXT, "
+ CLIENT_METADATA_URI_COLUMN + " TEXT, "
+ + CLIENT_METADATA_ADDITIONAL_ID_COLUMN + " TEXT, "
+ CLIENT_LAST_UPDATE_DATE_COLUMN + " INTEGER NOT NULL DEFAULT 0, "
+ CLIENT_PENDINGID_COLUMN + " INTEGER, "
+ FLAGS_COLUMN + " INTEGER, "
@@ -284,14 +284,15 @@ public class MetadataDbHelper extends SQLiteOpenHelper {
* @return the string representation of the URI
*/
public static String getMetadataUriAsString(final Context context, final String clientId) {
- SQLiteDatabase defaultDb = getDb(context, null);
- final Cursor cursor = defaultDb.query(CLIENT_TABLE_NAME,
- new String[] { CLIENT_METADATA_URI_COLUMN },
- CLIENT_CLIENT_ID_COLUMN + " = ?", new String[] { clientId },
+ SQLiteDatabase defaultDb = MetadataDbHelper.getDb(context, null);
+ final Cursor cursor = defaultDb.query(MetadataDbHelper.CLIENT_TABLE_NAME,
+ new String[] { MetadataDbHelper.CLIENT_METADATA_URI_COLUMN,
+ MetadataDbHelper.CLIENT_METADATA_ADDITIONAL_ID_COLUMN },
+ MetadataDbHelper.CLIENT_CLIENT_ID_COLUMN + " = ?", new String[] { clientId },
null, null, null, null);
try {
if (!cursor.moveToFirst()) return null;
- return cursor.getString(0); // Only one column, return it
+ return MetadataUriGetter.getUri(context, cursor.getString(0), cursor.getString(1));
} finally {
cursor.close();
}
@@ -300,7 +301,8 @@ public class MetadataDbHelper extends SQLiteOpenHelper {
/**
* Update the last metadata update time for all clients using a particular URI.
*
- * All clients using this metadata URI will be indicated as having been updated now.
+ * This method searches for all clients using a particular URI and updates the last
+ * update time for this client.
* The current time is used as the latest update time. This saved date will be what
* is returned henceforth by {@link #getLastUpdateDateForClient(Context, String)},
* until this method is called again.
@@ -309,13 +311,26 @@ public class MetadataDbHelper extends SQLiteOpenHelper {
* @param uri the metadata URI we just downloaded
*/
public static void saveLastUpdateTimeOfUri(final Context context, final String uri) {
- PrivateLog.log("Save last update time of URI : " + uri + " " + System.currentTimeMillis(),
- context);
+ PrivateLog.log("Save last update time of URI : " + uri + " " + System.currentTimeMillis());
final ContentValues values = new ContentValues();
values.put(CLIENT_LAST_UPDATE_DATE_COLUMN, System.currentTimeMillis());
final SQLiteDatabase defaultDb = getDb(context, null);
- defaultDb.update(CLIENT_TABLE_NAME, values,
- CLIENT_METADATA_URI_COLUMN + " = ?", new String[] { uri });
+ final Cursor cursor = MetadataDbHelper.queryClientIds(context);
+ if (null == cursor) return;
+ try {
+ if (!cursor.moveToFirst()) return;
+ do {
+ final String clientId = cursor.getString(0);
+ final String metadataUri =
+ MetadataDbHelper.getMetadataUriAsString(context, clientId);
+ if (metadataUri.equals(uri)) {
+ defaultDb.update(CLIENT_TABLE_NAME, values,
+ CLIENT_CLIENT_ID_COLUMN + " = ?", new String[] { clientId });
+ }
+ } while (cursor.moveToNext());
+ } finally {
+ cursor.close();
+ }
}
/**
@@ -730,11 +745,13 @@ public class MetadataDbHelper extends SQLiteOpenHelper {
/**
* Updates information relative to a specific client.
*
- * Updatable information includes only the metadata URI, but may be expanded in the future.
+ * Updatable information includes the metadata URI and the additional ID column. It may be
+ * expanded in the future.
* The passed values must include a client ID in the key CLIENT_CLIENT_ID_COLUMN, and it must
- * be equal to the string passed as an argument for clientId.
- * The passed values must also include a non-empty metadata URI in the
- * CLIENT_METADATA_URI_COLUMN column.
+ * be equal to the string passed as an argument for clientId. It may not be empty.
+ * The passed values must also include a non-null metadata URI in the
+ * CLIENT_METADATA_URI_COLUMN column, as well as a non-null additional ID in the
+ * CLIENT_METADATA_ADDITIONAL_ID_COLUMN. Both these strings may be empty.
* If any of the above is not complied with, this function returns without updating data.
*
* @param context the context, to open the database
@@ -746,10 +763,16 @@ public class MetadataDbHelper extends SQLiteOpenHelper {
// Sanity check the content values
final String valuesClientId = values.getAsString(CLIENT_CLIENT_ID_COLUMN);
final String valuesMetadataUri = values.getAsString(CLIENT_METADATA_URI_COLUMN);
- // Empty string is a valid client ID, but external apps may not configure it.
- // Empty string is a valid metadata URI if the client does not want updates.
- if (TextUtils.isEmpty(valuesClientId) || null == valuesMetadataUri) {
- // We need both these columns to be filled in
+ final String valuesMetadataAdditionalId =
+ values.getAsString(CLIENT_METADATA_ADDITIONAL_ID_COLUMN);
+ // Empty string is a valid client ID, but external apps may not configure it, so disallow
+ // both null and empty string.
+ // Empty string is a valid metadata URI if the client does not want updates, so allow
+ // empty string but disallow null.
+ // Empty string is a valid additional ID so allow empty string but disallow null.
+ if (TextUtils.isEmpty(valuesClientId) || null == valuesMetadataUri
+ || null == valuesMetadataAdditionalId) {
+ // We need all these columns to be filled in
Utils.l("Missing parameter for updateClientInfo");
return;
}
@@ -780,8 +803,9 @@ public class MetadataDbHelper extends SQLiteOpenHelper {
* Register a download ID for a specific metadata URI.
*
* This method should be called when a download for a metadata URI is starting. It will
- * register the download ID for all clients using this metadata URI into the database
- * for later retrieval by {@link #getDownloadRecordsForDownloadId(Context, long)}.
+ * search for all clients using this metadata URI and will register for each of them
+ * the download ID into the database for later retrieval by
+ * {@link #getDownloadRecordsForDownloadId(Context, long)}.
*
* @param context a context for opening databases
* @param uri the metadata URI
@@ -792,8 +816,22 @@ public class MetadataDbHelper extends SQLiteOpenHelper {
final ContentValues values = new ContentValues();
values.put(CLIENT_PENDINGID_COLUMN, downloadId);
final SQLiteDatabase defaultDb = getDb(context, "");
- defaultDb.update(CLIENT_TABLE_NAME, values,
- CLIENT_METADATA_URI_COLUMN + " = ?", new String[] { uri });
+ final Cursor cursor = MetadataDbHelper.queryClientIds(context);
+ if (null == cursor) return;
+ try {
+ if (!cursor.moveToFirst()) return;
+ do {
+ final String clientId = cursor.getString(0);
+ final String metadataUri =
+ MetadataDbHelper.getMetadataUriAsString(context, clientId);
+ if (metadataUri.equals(uri)) {
+ defaultDb.update(CLIENT_TABLE_NAME, values,
+ CLIENT_CLIENT_ID_COLUMN + " = ?", new String[] { clientId });
+ }
+ } while (cursor.moveToNext());
+ } finally {
+ cursor.close();
+ }
}
/**
diff --git a/java/src/com/android/inputmethod/dictionarypack/MetadataUriGetter.java b/java/src/com/android/inputmethod/dictionarypack/MetadataUriGetter.java
new file mode 100644
index 000000000..ed817658e
--- /dev/null
+++ b/java/src/com/android/inputmethod/dictionarypack/MetadataUriGetter.java
@@ -0,0 +1,33 @@
+/*
+ * 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.dictionarypack;
+
+import android.content.Context;
+
+/**
+ * Helper to get the metadata URI from its base URI and the additional ID, if any.
+ */
+public class MetadataUriGetter {
+ private MetadataUriGetter() {
+ // This helper class is not instantiable.
+ }
+
+ public static String getUri(final Context context, final String baseUri,
+ final String additionalId) {
+ return baseUri;
+ }
+}
diff --git a/java/src/com/android/inputmethod/dictionarypack/PrivateLog.java b/java/src/com/android/inputmethod/dictionarypack/PrivateLog.java
index 8593c1c9b..67dd7b9b7 100644
--- a/java/src/com/android/inputmethod/dictionarypack/PrivateLog.java
+++ b/java/src/com/android/inputmethod/dictionarypack/PrivateLog.java
@@ -16,7 +16,6 @@
package com.android.inputmethod.dictionarypack;
-import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
@@ -24,6 +23,7 @@ import android.database.sqlite.SQLiteOpenHelper;
import java.text.SimpleDateFormat;
import java.util.Date;
+import java.util.Locale;
/**
* Class to keep long-term log. This is inactive in production, and is only for debug purposes.
@@ -44,10 +44,10 @@ public class PrivateLog {
+ COLUMN_EVENT + " TEXT);";
private static final SimpleDateFormat sDateFormat =
- new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
+ new SimpleDateFormat("yyyy/MM/dd HH:mm:ss", Locale.US);
private static PrivateLog sInstance = new PrivateLog();
- private static DebugHelper mDebugHelper = null;
+ private static DebugHelper sDebugHelper = null;
private PrivateLog() {
}
@@ -55,8 +55,8 @@ public class PrivateLog {
public static synchronized PrivateLog getInstance(final Context context) {
if (!DEBUG) return sInstance;
synchronized(PrivateLog.class) {
- if (sInstance.mDebugHelper == null) {
- sInstance.mDebugHelper = new DebugHelper(context);
+ if (sDebugHelper == null) {
+ sDebugHelper = new DebugHelper(context);
}
return sInstance;
}
@@ -94,16 +94,9 @@ public class PrivateLog {
}
- public static void log(String event, Context context) {
+ public static void log(String event) {
if (!DEBUG) return;
- final SQLiteDatabase l = getInstance(context).mDebugHelper.getWritableDatabase();
- mDebugHelper.insert(l, event);
- }
-
- public static void log(String event, ContentProvider provider) {
- if (!DEBUG) return;
- final SQLiteDatabase l =
- getInstance(provider.getContext()).mDebugHelper.getWritableDatabase();
- mDebugHelper.insert(l, event);
+ final SQLiteDatabase l = sDebugHelper.getWritableDatabase();
+ DebugHelper.insert(l, event);
}
}
diff --git a/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java b/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java
index 3173e911b..3f917f13f 100644
--- a/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java
+++ b/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java
@@ -183,7 +183,7 @@ public final class UpdateHandler {
final String clientId = cursor.getString(0);
final String metadataUri =
MetadataDbHelper.getMetadataUriAsString(context, clientId);
- PrivateLog.log("Update for clientId " + Utils.s(clientId), context);
+ PrivateLog.log("Update for clientId " + Utils.s(clientId));
Utils.l("Update for clientId", clientId, " which uses URI ", metadataUri);
uris.add(metadataUri);
} while (cursor.moveToNext());
@@ -211,8 +211,13 @@ public final class UpdateHandler {
*/
private static void updateClientsWithMetadataUri(final Context context,
final boolean updateNow, final String metadataUri) {
- PrivateLog.log("Update for metadata URI " + Utils.s(metadataUri), context);
- final Request metadataRequest = new Request(Uri.parse(metadataUri));
+ PrivateLog.log("Update for metadata URI " + Utils.s(metadataUri));
+ // Adding a disambiguator to circumvent a bug in older versions of DownloadManager.
+ // DownloadManager also stupidly cuts the extension to replace with its own that it
+ // gets from the content-type. We need to circumvent this.
+ final String disambiguator = "#" + System.currentTimeMillis()
+ + com.android.inputmethod.latin.Utils.getVersionName(context) + ".json";
+ final Request metadataRequest = new Request(Uri.parse(metadataUri + disambiguator));
Utils.l("Request =", metadataRequest);
final Resources res = context.getResources();
@@ -257,7 +262,7 @@ public final class UpdateHandler {
// method will ignore it.
writeMetadataDownloadId(context, metadataUri, downloadId);
}
- PrivateLog.log("Requested download with id " + downloadId, context);
+ PrivateLog.log("Requested download with id " + downloadId);
}
/**
@@ -351,7 +356,13 @@ public final class UpdateHandler {
final int columnUri = cursor.getColumnIndex(DownloadManager.COLUMN_URI);
final int error = cursor.getInt(columnError);
status = cursor.getInt(columnStatus);
- uri = cursor.getString(columnUri);
+ final String uriWithAnchor = cursor.getString(columnUri);
+ int anchorIndex = uriWithAnchor.indexOf('#');
+ if (anchorIndex != -1) {
+ uri = uriWithAnchor.substring(0, anchorIndex);
+ } else {
+ uri = uriWithAnchor;
+ }
if (DownloadManager.STATUS_SUCCESSFUL != status) {
Log.e(TAG, "Permanent failure of download " + downloadId
+ " with error code: " + error);
@@ -404,7 +415,7 @@ public final class UpdateHandler {
/* package */ static void downloadFinished(final Context context, final Intent intent) {
// Get and check the ID of the file that was downloaded
final long fileId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, NOT_AN_ID);
- PrivateLog.log("Download finished with id " + fileId, context);
+ PrivateLog.log("Download finished with id " + fileId);
Utils.l("DownloadFinished with id", fileId);
if (NOT_AN_ID == fileId) return; // Spurious wake-up: ignore
@@ -491,7 +502,7 @@ public final class UpdateHandler {
private static void publishUpdateCycleCompletedEvent(final Context context) {
// Even if this is not successful, we have to publish the new state.
- PrivateLog.log("Publishing update cycle completed event", context);
+ PrivateLog.log("Publishing update cycle completed event");
Utils.l("Publishing update cycle completed event");
for (UpdateEventListener listener : linkedCopyOfList(sUpdateEventListeners)) {
listener.updateCycleCompleted();
@@ -582,7 +593,7 @@ public final class UpdateHandler {
}
Utils.l("Downloaded metadata :", newMetadata);
- PrivateLog.log("Downloaded metadata\n" + newMetadata, context);
+ PrivateLog.log("Downloaded metadata\n" + newMetadata);
final ActionBatch actions = computeUpgradeTo(context, clientId, newMetadata);
// TODO: Check with UX how we should report to the user
@@ -610,7 +621,7 @@ public final class UpdateHandler {
MetadataDbHelper.DESCRIPTION_COLUMN), "for", downloadRecord.mClientId);
PrivateLog.log("Downloaded a new word list with description : "
+ downloadRecord.mAttributes.getAsString(MetadataDbHelper.DESCRIPTION_COLUMN)
- + " for " + downloadRecord.mClientId, context);
+ + " for " + downloadRecord.mClientId);
final String locale =
downloadRecord.mAttributes.getAsString(MetadataDbHelper.LOCALE_COLUMN);
diff --git a/java/src/com/android/inputmethod/dictionarypack/WordListPreference.java b/java/src/com/android/inputmethod/dictionarypack/WordListPreference.java
index 0d923ae01..451a0fb82 100644
--- a/java/src/com/android/inputmethod/dictionarypack/WordListPreference.java
+++ b/java/src/com/android/inputmethod/dictionarypack/WordListPreference.java
@@ -16,14 +16,15 @@
package com.android.inputmethod.dictionarypack;
-import android.app.Dialog;
import android.content.Context;
import android.content.SharedPreferences;
-import android.preference.DialogPreference;
+import android.preference.Preference;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
-import android.widget.Button;
+import android.view.ViewParent;
+import android.widget.ListView;
+import android.widget.TextView;
import com.android.inputmethod.latin.R;
@@ -36,7 +37,7 @@ import java.util.Locale;
* pack. Upon being pressed, it displays a menu to allow the user to install, disable,
* enable or delete it as appropriate for the current state of the word list.
*/
-public final class WordListPreference extends DialogPreference {
+public final class WordListPreference extends Preference {
static final private String TAG = WordListPreference.class.getSimpleName();
// What to display in the "status" field when we receive unknown data as a status from
@@ -57,26 +58,32 @@ public final class WordListPreference extends DialogPreference {
// The metadata word list id and version of this word list.
public final String mWordlistId;
public final int mVersion;
+ public final Locale mLocale;
+ public final String mDescription;
// The status
public int mStatus;
+ // The size of the dictionary file
+ private final int mFilesize;
- // Animation directions
- static final private int ANIMATION_IN = 1;
- static final private int ANIMATION_OUT = 2;
-
- private static Button sLastClickedActionButton = null;
+ private final DictionaryListInterfaceState mInterfaceState;
private final OnWordListPreferenceClick mPreferenceClickHandler =
new OnWordListPreferenceClick();
private final OnActionButtonClick mActionButtonClickHandler =
new OnActionButtonClick();
- public WordListPreference(final Context context, final String clientId, final String wordlistId,
- final int version, final Locale locale, final String description, final int status) {
+ public WordListPreference(final Context context,
+ final DictionaryListInterfaceState dictionaryListInterfaceState, final String clientId,
+ final String wordlistId, final int version, final Locale locale,
+ final String description, final int status, final int filesize) {
super(context, null);
mContext = context;
+ mInterfaceState = dictionaryListInterfaceState;
mClientId = clientId;
mVersion = version;
mWordlistId = wordlistId;
+ mFilesize = filesize;
+ mLocale = locale;
+ mDescription = description;
setLayoutResource(R.layout.dictionary_line);
@@ -89,12 +96,6 @@ public final class WordListPreference extends DialogPreference {
if (status == mStatus) return;
mStatus = status;
setSummary(getSummary(status));
- // If we are currently displaying the dialog, we should update it, or at least
- // dismiss it.
- final Dialog dialog = getDialog();
- if (null != dialog) {
- dialog.dismiss();
- }
}
private String getSummary(final int status) {
@@ -117,29 +118,31 @@ public final class WordListPreference extends DialogPreference {
}
}
+ // The table below needs to be kept in sync with MetadataDbHelper.STATUS_* since it uses
+ // the values as indices.
private static final int sStatusActionList[][] = {
// MetadataDbHelper.STATUS_UNKNOWN
{},
// MetadataDbHelper.STATUS_AVAILABLE
- { R.string.install_dict, ACTION_ENABLE_DICT },
+ { ButtonSwitcher.STATUS_INSTALL, ACTION_ENABLE_DICT },
// MetadataDbHelper.STATUS_DOWNLOADING
- { R.string.cancel_download_dict, ACTION_DISABLE_DICT },
+ { ButtonSwitcher.STATUS_CANCEL, ACTION_DISABLE_DICT },
// MetadataDbHelper.STATUS_INSTALLED
- { R.string.delete_dict, ACTION_DELETE_DICT },
+ { ButtonSwitcher.STATUS_DELETE, ACTION_DELETE_DICT },
// MetadataDbHelper.STATUS_DISABLED
- { R.string.delete_dict, ACTION_DELETE_DICT },
+ { ButtonSwitcher.STATUS_DELETE, ACTION_DELETE_DICT },
// MetadataDbHelper.STATUS_DELETING
// We show 'install' because the file is supposed to be deleted.
// The user may reinstall it.
- { R.string.install_dict, ACTION_ENABLE_DICT }
+ { ButtonSwitcher.STATUS_INSTALL, ACTION_ENABLE_DICT }
};
- private CharSequence getButtonLabel(final int status) {
+ private int getButtonSwitcherStatus(final int status) {
if (status >= sStatusActionList.length) {
Log.e(TAG, "Unknown status " + status);
- return "";
+ return ButtonSwitcher.STATUS_NO_BUTTON;
}
- return mContext.getString(sStatusActionList[status][0]);
+ return sStatusActionList[status][0];
}
private static int getActionIdFromStatusAndMenuEntry(final int status) {
@@ -194,36 +197,70 @@ public final class WordListPreference extends DialogPreference {
protected void onBindView(final View view) {
super.onBindView(view);
((ViewGroup)view).setLayoutTransition(null);
- final Button button = (Button)view.findViewById(R.id.wordlist_button);
- button.setText(getButtonLabel(mStatus));
- button.setVisibility(View.INVISIBLE);
- button.setOnClickListener(mActionButtonClickHandler);
+
+ final DictionaryDownloadProgressBar progressBar =
+ (DictionaryDownloadProgressBar)view.findViewById(R.id.dictionary_line_progress_bar);
+ final TextView status = (TextView)view.findViewById(android.R.id.summary);
+ progressBar.setIds(mClientId, mWordlistId);
+ progressBar.setMax(mFilesize);
+ final boolean showProgressBar = (MetadataDbHelper.STATUS_DOWNLOADING == mStatus);
+ status.setVisibility(showProgressBar ? View.INVISIBLE : View.VISIBLE);
+ progressBar.setVisibility(showProgressBar ? View.VISIBLE : View.INVISIBLE);
+
+ final ButtonSwitcher buttonSwitcher =
+ (ButtonSwitcher)view.findViewById(R.id.wordlist_button_switcher);
+ if (mInterfaceState.isOpen(mWordlistId)) {
+ // The button is open.
+ final int previousStatus = mInterfaceState.getStatus(mWordlistId);
+ buttonSwitcher.setStatusAndUpdateVisuals(getButtonSwitcherStatus(previousStatus));
+ if (previousStatus != mStatus) {
+ // We come here if the status has changed since last time. We need to animate
+ // the transition.
+ buttonSwitcher.setStatusAndUpdateVisuals(getButtonSwitcherStatus(mStatus));
+ mInterfaceState.setOpen(mWordlistId, mStatus);
+ }
+ } else {
+ // The button is closed.
+ buttonSwitcher.setStatusAndUpdateVisuals(ButtonSwitcher.STATUS_NO_BUTTON);
+ }
+ buttonSwitcher.setInternalOnClickListener(mActionButtonClickHandler);
view.setOnClickListener(mPreferenceClickHandler);
}
private class OnWordListPreferenceClick implements View.OnClickListener {
@Override
public void onClick(final View v) {
- final Button button = (Button)v.findViewById(R.id.wordlist_button);
- if (null != sLastClickedActionButton) {
- animateButton(sLastClickedActionButton, ANIMATION_OUT);
+ // Note : v is the preference view
+ final ViewParent parent = v.getParent();
+ // Just in case something changed in the framework, test for the concrete class
+ if (!(parent instanceof ListView)) return;
+ final ListView listView = (ListView)parent;
+ final int indexToOpen;
+ // Close all first, we'll open back any item that needs to be open.
+ final boolean wasOpen = mInterfaceState.isOpen(mWordlistId);
+ mInterfaceState.closeAll();
+ if (wasOpen) {
+ // This button being shown. Take note that we don't want to open any button in the
+ // loop below.
+ indexToOpen = -1;
+ } else {
+ // This button was not being shown. Open it, and remember the index of this
+ // child as the one to open in the following loop.
+ mInterfaceState.setOpen(mWordlistId, mStatus);
+ indexToOpen = listView.indexOfChild(v);
+ }
+ final int lastDisplayedIndex =
+ listView.getLastVisiblePosition() - listView.getFirstVisiblePosition();
+ // The "lastDisplayedIndex" is actually displayed, hence the <=
+ for (int i = 0; i <= lastDisplayedIndex; ++i) {
+ final ButtonSwitcher buttonSwitcher = (ButtonSwitcher)listView.getChildAt(i)
+ .findViewById(R.id.wordlist_button_switcher);
+ if (i == indexToOpen) {
+ buttonSwitcher.setStatusAndUpdateVisuals(getButtonSwitcherStatus(mStatus));
+ } else {
+ buttonSwitcher.setStatusAndUpdateVisuals(ButtonSwitcher.STATUS_NO_BUTTON);
+ }
}
- animateButton(button, ANIMATION_IN);
- sLastClickedActionButton = button;
- }
- }
-
- private void animateButton(final Button button, final int direction) {
- final float outerX = ((View)button.getParent()).getWidth();
- final float innerX = button.getX() - button.getTranslationX();
- if (View.INVISIBLE == button.getVisibility()) {
- button.setTranslationX(outerX - innerX);
- button.setVisibility(View.VISIBLE);
- }
- if (ANIMATION_IN == direction) {
- button.animate().translationX(0);
- } else {
- button.animate().translationX(outerX - innerX);
}
}
diff --git a/java/src/com/android/inputmethod/keyboard/Key.java b/java/src/com/android/inputmethod/keyboard/Key.java
index d160038ad..1550e77e3 100644
--- a/java/src/com/android/inputmethod/keyboard/Key.java
+++ b/java/src/com/android/inputmethod/keyboard/Key.java
@@ -105,7 +105,7 @@ public class Key implements Comparable<Key> {
/** Hit bounding box of the key */
public final Rect mHitBox = new Rect();
- /** More keys */
+ /** More keys. It is guaranteed that this is null or an array of one or more elements */
public final MoreKeySpec[] mMoreKeys;
/** More keys column number and flags */
private final int mMoreKeysColumnAndFlags;
diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardActionListener.java b/java/src/com/android/inputmethod/keyboard/KeyboardActionListener.java
index c76acd126..9eeee5baf 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyboardActionListener.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardActionListener.java
@@ -27,8 +27,9 @@ public interface KeyboardActionListener {
*
* @param primaryCode the unicode of the key being pressed. If the touch is not on a valid key,
* the value will be zero.
+ * @param isSinglePointer true if pressing has occurred while no other key is being pressed.
*/
- public void onPressKey(int primaryCode);
+ public void onPressKey(int primaryCode, boolean isSinglePointer);
/**
* Called when the user releases a key. This is sent after the {@link #onCodeInput} is called.
@@ -88,14 +89,21 @@ public interface KeyboardActionListener {
public void onCancelInput();
/**
+ * Called when user finished sliding key input.
+ */
+ public void onFinishSlidingInput();
+
+ /**
* Send a non-"code input" custom request to the listener.
* @return true if the request has been consumed, false otherwise.
*/
public boolean onCustomRequest(int requestCode);
public static class Adapter implements KeyboardActionListener {
+ public static final Adapter EMPTY_LISTENER = new Adapter();
+
@Override
- public void onPressKey(int primaryCode) {}
+ public void onPressKey(int primaryCode, boolean isSinglePointer) {}
@Override
public void onReleaseKey(int primaryCode, boolean withSliding) {}
@Override
@@ -113,6 +121,8 @@ public interface KeyboardActionListener {
@Override
public void onCancelInput() {}
@Override
+ public void onFinishSlidingInput() {}
+ @Override
public boolean onCustomRequest(int requestCode) {
return false;
}
diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardId.java b/java/src/com/android/inputmethod/keyboard/KeyboardId.java
index ee8ee9a4f..aa27067bc 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyboardId.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardId.java
@@ -56,13 +56,8 @@ public final class KeyboardId {
public static final int ELEMENT_PHONE_SYMBOLS = 8;
public static final int ELEMENT_NUMBER = 9;
- public static final int FORM_FACTOR_PHONE = 0;
- public static final int FORM_FACTOR_TABLET7 = 1;
- public static final int FORM_FACTOR_TABLET10 = 2;
-
public final InputMethodSubtype mSubtype;
public final Locale mLocale;
- public final int mDeviceFormFactor;
// TODO: Remove this member. It is used only for logging purpose.
public final int mOrientation;
public final int mWidth;
@@ -82,7 +77,6 @@ public final class KeyboardId {
public KeyboardId(final int elementId, final KeyboardLayoutSet.Params params) {
mSubtype = params.mSubtype;
mLocale = SubtypeLocale.getSubtypeLocale(mSubtype);
- mDeviceFormFactor = params.mDeviceFormFactor;
mOrientation = params.mOrientation;
mWidth = params.mKeyboardWidth;
mHeight = params.mKeyboardHeight;
@@ -107,7 +101,6 @@ public final class KeyboardId {
private static int computeHashCode(final KeyboardId id) {
return Arrays.hashCode(new Object[] {
- id.mDeviceFormFactor,
id.mOrientation,
id.mElementId,
id.mMode,
@@ -130,8 +123,7 @@ public final class KeyboardId {
private boolean equals(final KeyboardId other) {
if (other == this)
return true;
- return other.mDeviceFormFactor == mDeviceFormFactor
- && other.mOrientation == mOrientation
+ return other.mOrientation == mOrientation
&& other.mElementId == mElementId
&& other.mMode == mMode
&& other.mWidth == mWidth
@@ -195,11 +187,11 @@ public final class KeyboardId {
public String toString() {
final String orientation = (mOrientation == Configuration.ORIENTATION_PORTRAIT)
? "port" : "land";
- return String.format("[%s %s:%s %s-%s:%dx%d %s %s %s%s%s%s%s%s%s%s%s]",
+ return String.format("[%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),
- deviceFormFactor(mDeviceFormFactor), orientation, mWidth, mHeight,
+ orientation, mWidth, mHeight,
modeName(mMode),
imeAction(),
(navigateNext() ? "navigateNext" : ""),
@@ -238,15 +230,6 @@ public final class KeyboardId {
}
}
- public static String deviceFormFactor(final int deviceFormFactor) {
- switch (deviceFormFactor) {
- case FORM_FACTOR_PHONE: return "phone";
- case FORM_FACTOR_TABLET7: return "tablet7";
- case FORM_FACTOR_TABLET10: return "tablet10";
- default: return null;
- }
- }
-
public static String modeName(final int mode) {
switch (mode) {
case MODE_TEXT: return "text";
diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java b/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java
index 5e68c7067..1fe23a330 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java
@@ -29,17 +29,18 @@ import android.content.res.TypedArray;
import android.content.res.XmlResourceParser;
import android.text.InputType;
import android.text.TextUtils;
+import android.util.DisplayMetrics;
import android.util.Log;
import android.util.SparseArray;
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;
import com.android.inputmethod.keyboard.internal.KeysCache;
+import com.android.inputmethod.latin.AdditionalSubtype;
import com.android.inputmethod.latin.CollectionUtils;
import com.android.inputmethod.latin.InputAttributes;
import com.android.inputmethod.latin.InputTypeUtils;
@@ -72,6 +73,8 @@ public final class KeyboardLayoutSet {
private static final String TAG_ELEMENT = "Element";
private static final String KEYBOARD_LAYOUT_SET_RESOURCE_PREFIX = "keyboard_layout_set_";
+ private static final int SPELLCHECKER_DUMMY_KEYBOARD_WIDTH = 480;
+ private static final int SPELLCHECKER_DUMMY_KEYBOARD_HEIGHT = 800;
private final Context mContext;
private final Params mParams;
@@ -106,7 +109,6 @@ public final class KeyboardLayoutSet {
boolean mNoSettingsKey;
boolean mLanguageSwitchKeyEnabled;
InputMethodSubtype mSubtype;
- int mDeviceFormFactor;
int mOrientation;
int mKeyboardWidth;
int mKeyboardHeight;
@@ -217,10 +219,8 @@ public final class KeyboardLayoutSet {
mPackageName, NO_SETTINGS_KEY, mEditorInfo);
}
- public Builder setScreenGeometry(final int deviceFormFactor, final int widthPixels,
- final int heightPixels) {
+ public Builder setScreenGeometry(final int widthPixels, final int heightPixels) {
final Params params = mParams;
- params.mDeviceFormFactor = deviceFormFactor;
params.mOrientation = (heightPixels > widthPixels)
? Configuration.ORIENTATION_PORTRAIT : Configuration.ORIENTATION_LANDSCAPE;
setDefaultKeyboardSize(widthPixels, heightPixels);
@@ -285,8 +285,7 @@ public final class KeyboardLayoutSet {
return this;
}
- @UsedForTesting
- public void disableTouchPositionCorrectionDataForTest() {
+ public void disableTouchPositionCorrectionData() {
mParams.mDisableTouchPositionCorrectionDataForTest = true;
}
@@ -416,4 +415,47 @@ public final class KeyboardLayoutSet {
}
}
}
+
+ public static KeyboardLayoutSet createKeyboardSetForSpellChecker(final Context context,
+ final String locale, final String layout) {
+ final InputMethodSubtype subtype =
+ AdditionalSubtype.createAdditionalSubtype(locale, layout, null);
+ return createKeyboardSet(context, subtype, SPELLCHECKER_DUMMY_KEYBOARD_WIDTH,
+ SPELLCHECKER_DUMMY_KEYBOARD_HEIGHT, false);
+ }
+
+ public static KeyboardLayoutSet createKeyboardSetForTest(final Context context,
+ final InputMethodSubtype subtype, final int orientation,
+ final boolean testCasesHaveTouchCoordinates) {
+ final DisplayMetrics dm = context.getResources().getDisplayMetrics();
+ final int width;
+ final int height;
+ if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
+ width = Math.max(dm.widthPixels, dm.heightPixels);
+ height = Math.min(dm.widthPixels, dm.heightPixels);
+ } else if (orientation == Configuration.ORIENTATION_PORTRAIT) {
+ width = Math.min(dm.widthPixels, dm.heightPixels);
+ height = Math.max(dm.widthPixels, dm.heightPixels);
+ } else {
+ throw new RuntimeException("Orientation should be ORIENTATION_LANDSCAPE or "
+ + "ORIENTATION_PORTRAIT: orientation=" + orientation);
+ }
+ return createKeyboardSet(context, subtype, width, height, testCasesHaveTouchCoordinates);
+ }
+
+ private static KeyboardLayoutSet createKeyboardSet(final Context context,
+ final InputMethodSubtype subtype, final int width, final int height,
+ final boolean testCasesHaveTouchCoordinates) {
+ final EditorInfo editorInfo = new EditorInfo();
+ editorInfo.inputType = InputType.TYPE_CLASS_TEXT;
+ final KeyboardLayoutSet.Builder builder = new KeyboardLayoutSet.Builder(
+ context, editorInfo);
+ builder.setScreenGeometry(width, height);
+ builder.setSubtype(subtype);
+ if (!testCasesHaveTouchCoordinates) {
+ // For spell checker and tests
+ builder.disableTouchPositionCorrectionData();
+ }
+ return builder.build();
+ }
}
diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java b/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java
index 4e41b77ce..ad08d6477 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java
@@ -142,8 +142,7 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions {
mThemeContext, editorInfo);
final Resources res = mThemeContext.getResources();
final DisplayMetrics dm = res.getDisplayMetrics();
- builder.setScreenGeometry(res.getInteger(R.integer.config_device_form_factor),
- dm.widthPixels, dm.heightPixels);
+ builder.setScreenGeometry(dm.widthPixels, dm.heightPixels);
builder.setSubtype(mSubtypeSwitcher.getCurrentSubtype());
builder.setOptions(
settingsValues.isVoiceKeyEnabled(editorInfo),
@@ -217,19 +216,19 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions {
mState.onResetKeyboardStateToAlphabet();
}
- public void onPressKey(final int code) {
+ public void onPressKey(final int code, final boolean isSinglePointer) {
if (isVibrateAndSoundFeedbackRequired()) {
mFeedbackManager.hapticAndAudioFeedback(code, mKeyboardView);
}
- mState.onPressKey(code, isSinglePointer(), mLatinIME.getCurrentAutoCapsState());
+ mState.onPressKey(code, isSinglePointer, mLatinIME.getCurrentAutoCapsState());
}
public void onReleaseKey(final int code, final boolean withSliding) {
mState.onReleaseKey(code, withSliding);
}
- public void onCancelInput() {
- mState.onCancelInput(isSinglePointer());
+ public void onFinishSlidingInput() {
+ mState.onFinishSlidingInput();
}
// Implements {@link KeyboardState.SwitchActions}.
@@ -347,15 +346,11 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions {
return mKeyboardView != null && !mKeyboardView.isInSlidingKeyInput();
}
- private boolean isSinglePointer() {
- return mKeyboardView != null && mKeyboardView.getPointerCount() == 1;
- }
-
/**
* Updates state machine to figure out when to automatically switch back to the previous mode.
*/
public void onCodeInput(final int code) {
- mState.onCodeInput(code, isSinglePointer(), mLatinIME.getCurrentAutoCapsState());
+ mState.onCodeInput(code, mLatinIME.getCurrentAutoCapsState());
}
public MainKeyboardView getMainKeyboardView() {
diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardView.java b/java/src/com/android/inputmethod/keyboard/KeyboardView.java
index e4e75c342..7941fcba2 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyboardView.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardView.java
@@ -497,7 +497,7 @@ public class KeyboardView extends View {
}
}
- if (key.hasPopupHint() && key.mMoreKeys != null && key.mMoreKeys.length > 0) {
+ if (key.hasPopupHint() && key.mMoreKeys != null) {
drawKeyPopupHint(key, canvas, paint, params);
}
}
diff --git a/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java b/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java
index ba78d014a..6c6fc6157 100644
--- a/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java
+++ b/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java
@@ -333,6 +333,10 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack
private static void cancelAndStartAnimators(final ObjectAnimator animatorToCancel,
final ObjectAnimator animatorToStart) {
+ if (animatorToCancel == null || animatorToStart == null) {
+ // TODO: Stop using null as a no-operation animator.
+ return;
+ }
float startFraction = 0.0f;
if (animatorToCancel.isStarted()) {
animatorToCancel.cancel();
@@ -366,7 +370,9 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack
// When user hits the space or the enter key, just cancel the while-typing timer.
final int typedCode = typedKey.mCode;
if (typedCode == Constants.CODE_SPACE || typedCode == Constants.CODE_ENTER) {
- startWhileTypingFadeinAnimation(keyboardView);
+ if (isTyping) {
+ startWhileTypingFadeinAnimation(keyboardView);
+ }
return;
}
@@ -581,6 +587,7 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack
private ObjectAnimator loadObjectAnimator(final int resId, final Object target) {
if (resId == 0) {
+ // TODO: Stop returning null.
return null;
}
final ObjectAnimator animator = (ObjectAnimator)AnimatorInflater.loadAnimator(
@@ -609,8 +616,18 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack
@ExternallyReferenced
public void setAltCodeKeyWhileTypingAnimAlpha(final int alpha) {
+ if (mAltCodeKeyWhileTypingAnimAlpha == alpha) {
+ return;
+ }
+ // Update the visual of alt-code-key-while-typing.
mAltCodeKeyWhileTypingAnimAlpha = alpha;
- updateAltCodeKeyWhileTyping();
+ final Keyboard keyboard = getKeyboard();
+ if (keyboard == null) {
+ return;
+ }
+ for (final Key key : keyboard.mAltCodeKeysWhileTyping) {
+ invalidateKey(key);
+ }
}
public void setKeyboardActionListener(final KeyboardActionListener listener) {
@@ -893,10 +910,10 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack
mSlidingKeyInputPreview.dismissSlidingKeyInputPreview();
}
- public void setGesturePreviewMode(final boolean drawsGesturePreviewTrail,
+ public void setGesturePreviewMode(final boolean drawsGestureTrail,
final boolean drawsGestureFloatingPreviewText) {
mGestureFloatingPreviewText.setPreviewEnabled(drawsGestureFloatingPreviewText);
- mGestureTrailsPreview.setPreviewEnabled(drawsGesturePreviewTrail);
+ mGestureTrailsPreview.setPreviewEnabled(drawsGestureTrail);
}
public void showGestureFloatingPreviewText(final SuggestedWords suggestedWords) {
@@ -910,7 +927,7 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack
}
@Override
- public void showGesturePreviewTrail(final PointerTracker tracker) {
+ public void showGestureTrail(final PointerTracker tracker) {
locatePreviewPlacerView();
mGestureFloatingPreviewText.setPreviewPosition(tracker);
mGestureTrailsPreview.setPreviewPosition(tracker);
@@ -1054,6 +1071,7 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack
@Override
public void onShowMoreKeysPanel(final MoreKeysPanel panel) {
+ locatePreviewPlacerView();
if (isShowingMoreKeysPanel()) {
onDismissMoreKeysPanel();
}
@@ -1082,10 +1100,6 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack
return false;
}
- public int getPointerCount() {
- return mOldPointerCount;
- }
-
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
if (AccessibilityUtils.getInstance().isTouchExplorationEnabled()) {
@@ -1189,10 +1203,12 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack
if (ENABLE_USABILITY_STUDY_LOG) {
writeUsabilityStudyLog(me, action, eventTime, i, pointerId, px, py);
}
- if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
- ResearchLogger.mainKeyboardView_processMotionEvent(
- me, action, eventTime, i, pointerId, px, py);
- }
+ // TODO: This seems to be no longer necessary, and confusing because it leads to
+ // duplicate MotionEvents being recorded.
+ // if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
+ // ResearchLogger.mainKeyboardView_processMotionEvent(
+ // me, action, eventTime, i, pointerId, px, py);
+ // }
}
} else {
final PointerTracker tracker = PointerTracker.getPointerTracker(id, this);
@@ -1274,16 +1290,6 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack
invalidateKey(shortcutKey);
}
- private void updateAltCodeKeyWhileTyping() {
- final Keyboard keyboard = getKeyboard();
- if (keyboard == null) {
- return;
- }
- for (final Key key : keyboard.mAltCodeKeysWhileTyping) {
- invalidateKey(key);
- }
- }
-
public void startDisplayLanguageOnSpacebar(final boolean subtypeChanged,
final boolean needsToDisplayLanguage, final boolean hasMultipleEnabledIMEsOrSubtypes) {
mNeedsToDisplayLanguage = needsToDisplayLanguage;
diff --git a/java/src/com/android/inputmethod/keyboard/PointerTracker.java b/java/src/com/android/inputmethod/keyboard/PointerTracker.java
index 91c4319e7..174239325 100644
--- a/java/src/com/android/inputmethod/keyboard/PointerTracker.java
+++ b/java/src/com/android/inputmethod/keyboard/PointerTracker.java
@@ -25,6 +25,7 @@ import com.android.inputmethod.accessibility.AccessibilityUtils;
import com.android.inputmethod.keyboard.internal.GestureStroke;
import com.android.inputmethod.keyboard.internal.GestureStroke.GestureStrokeParams;
import com.android.inputmethod.keyboard.internal.GestureStrokeWithPreviewPoints;
+import com.android.inputmethod.keyboard.internal.GestureStrokeWithPreviewPoints.GestureStrokePreviewParams;
import com.android.inputmethod.keyboard.internal.PointerTrackerQueue;
import com.android.inputmethod.latin.CollectionUtils;
import com.android.inputmethod.latin.Constants;
@@ -83,7 +84,7 @@ public final class PointerTracker implements PointerTrackerQueue.Element {
public void dismissKeyPreview(PointerTracker tracker);
public void showSlidingKeyInputPreview(PointerTracker tracker);
public void dismissSlidingKeyInputPreview();
- public void showGesturePreviewTrail(PointerTracker tracker);
+ public void showGestureTrail(PointerTracker tracker);
}
public interface TimerProxy {
@@ -161,6 +162,7 @@ public final class PointerTracker implements PointerTrackerQueue.Element {
// Parameters for pointer handling.
private static PointerTrackerParams sParams;
private static GestureStrokeParams sGestureStrokeParams;
+ private static GestureStrokePreviewParams sGesturePreviewParams;
private static boolean sNeedsPhantomSuddenMoveEventHack;
// Move this threshold to resource.
// TODO: Device specific parameter would be better for device specific hack?
@@ -176,7 +178,7 @@ public final class PointerTracker implements PointerTrackerQueue.Element {
private DrawingProxy mDrawingProxy;
private TimerProxy mTimerProxy;
private KeyDetector mKeyDetector;
- private KeyboardActionListener mListener = EMPTY_LISTENER;
+ private KeyboardActionListener mListener = KeyboardActionListener.Adapter.EMPTY_LISTENER;
private Keyboard mKeyboard;
private int mPhantonSuddenMoveThreshold;
@@ -318,8 +320,8 @@ public final class PointerTracker implements PointerTrackerQueue.Element {
// true if keyboard layout has been changed.
private boolean mKeyboardLayoutHasBeenChanged;
- // true if this pointer is no longer tracking touch event.
- private boolean mIsTrackingCanceled;
+ // true if this pointer is no longer triggering any action because it has been canceled.
+ private boolean mIsTrackingForActionDisabled;
// the more keys panel currently being shown. equals null if no panel is active.
private MoreKeysPanel mMoreKeysPanel;
@@ -333,22 +335,20 @@ public final class PointerTracker implements PointerTrackerQueue.Element {
// true if a sliding key input is allowed.
private boolean mIsAllowedSlidingKeyInput;
- // Empty {@link KeyboardActionListener}
- private static final KeyboardActionListener EMPTY_LISTENER =
- new KeyboardActionListener.Adapter();
-
private final GestureStrokeWithPreviewPoints mGestureStrokeWithPreviewPoints;
public static void init(final boolean needsPhantomSuddenMoveEventHack) {
sNeedsPhantomSuddenMoveEventHack = needsPhantomSuddenMoveEventHack;
sParams = PointerTrackerParams.DEFAULT;
sGestureStrokeParams = GestureStrokeParams.DEFAULT;
+ sGesturePreviewParams = GestureStrokePreviewParams.DEFAULT;
sTimeRecorder = new TimeRecorder(sParams, sGestureStrokeParams);
}
public static void setParameters(final TypedArray mainKeyboardViewAttr) {
sParams = new PointerTrackerParams(mainKeyboardViewAttr);
sGestureStrokeParams = new GestureStrokeParams(mainKeyboardViewAttr);
+ sGesturePreviewParams = new GestureStrokePreviewParams(mainKeyboardViewAttr);
sTimeRecorder = new TimeRecorder(sParams, sGestureStrokeParams);
}
@@ -432,7 +432,7 @@ public final class PointerTracker implements PointerTrackerQueue.Element {
}
mPointerId = id;
mGestureStrokeWithPreviewPoints = new GestureStrokeWithPreviewPoints(
- id, sGestureStrokeParams);
+ id, sGestureStrokeParams, sGesturePreviewParams);
setKeyDetectorInner(handler.getKeyDetector());
mListener = handler.getKeyboardActionListener();
mDrawingProxy = handler.getDrawingProxy();
@@ -441,7 +441,11 @@ public final class PointerTracker implements PointerTrackerQueue.Element {
// Returns true if keyboard has been changed by this callback.
private boolean callListenerOnPressAndCheckKeyboardLayoutChange(final Key key) {
- if (sInGesture || mIsDetectingGesture) {
+ // While gesture input is going on, this method should be a no-operation. But when gesture
+ // input has been canceled, <code>sInGesture</code> and <code>mIsDetectingGesture</code>
+ // are set to false. To keep this method is a no-operation,
+ // <code>mIsTrackingForActionDisabled</code> should also be taken account of.
+ if (sInGesture || mIsDetectingGesture || mIsTrackingForActionDisabled) {
return false;
}
final boolean ignoreModifierKey = mIsInSlidingKeyInput && key.isModifier();
@@ -455,7 +459,7 @@ public final class PointerTracker implements PointerTrackerQueue.Element {
return false;
}
if (key.isEnabled()) {
- mListener.onPressKey(key.mCode);
+ mListener.onPressKey(key.mCode, getActivePointerTrackerCount() == 1);
final boolean keyboardLayoutHasBeenChanged = mKeyboardLayoutHasBeenChanged;
mKeyboardLayoutHasBeenChanged = false;
mTimerProxy.startTypingStateTimer(key);
@@ -500,7 +504,8 @@ public final class PointerTracker implements PointerTrackerQueue.Element {
// primaryCode is different from {@link Key#mCode}.
private void callListenerOnRelease(final Key key, final int primaryCode,
final boolean withSliding) {
- if (sInGesture || mIsDetectingGesture) {
+ // See the comment at {@link #callListenerOnPressAndCheckKeyboardLayoutChange(Key}}.
+ if (sInGesture || mIsDetectingGesture || mIsTrackingForActionDisabled) {
return;
}
final boolean ignoreModifierKey = mIsInSlidingKeyInput && key.isModifier();
@@ -522,6 +527,13 @@ public final class PointerTracker implements PointerTrackerQueue.Element {
}
}
+ private void callListenerOnFinishSlidingInput() {
+ if (DEBUG_LISTENER) {
+ Log.d(TAG, String.format("[%d] onFinishSlidingInput", mPointerId));
+ }
+ mListener.onFinishSlidingInput();
+ }
+
private void callListenerOnCancelInput() {
if (DEBUG_LISTENER) {
Log.d(TAG, String.format("[%d] onCancelInput", mPointerId));
@@ -732,7 +744,7 @@ public final class PointerTracker implements PointerTrackerQueue.Element {
dismissAllMoreKeysPanels();
}
mTimerProxy.cancelLongPressTimer();
- mDrawingProxy.showGesturePreviewTrail(this);
+ mDrawingProxy.showGestureTrail(this);
}
public void updateBatchInputByTimer(final long eventTime) {
@@ -745,10 +757,10 @@ public final class PointerTracker implements PointerTrackerQueue.Element {
if (key != null) {
updateBatchInput(eventTime);
}
- if (mIsTrackingCanceled) {
+ if (mIsTrackingForActionDisabled) {
return;
}
- mDrawingProxy.showGesturePreviewTrail(this);
+ mDrawingProxy.showGestureTrail(this);
}
private void updateBatchInput(final long eventTime) {
@@ -777,7 +789,7 @@ public final class PointerTracker implements PointerTrackerQueue.Element {
sInGesture = false;
sTimeRecorder.onEndBatchInput(eventTime);
mTimerProxy.cancelAllUpdateBatchInputTimers();
- if (!mIsTrackingCanceled) {
+ if (!mIsTrackingForActionDisabled) {
if (DEBUG_LISTENER) {
Log.d(TAG, String.format("[%d] onEndBatchInput : batchPoints=%d",
mPointerId, sAggregratedPointers.getPointerSize()));
@@ -786,10 +798,10 @@ public final class PointerTracker implements PointerTrackerQueue.Element {
}
}
}
- if (mIsTrackingCanceled) {
+ if (mIsTrackingForActionDisabled) {
return;
}
- mDrawingProxy.showGesturePreviewTrail(this);
+ mDrawingProxy.showGestureTrail(this);
}
private void cancelBatchInput() {
@@ -846,7 +858,7 @@ public final class PointerTracker implements PointerTrackerQueue.Element {
if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
ResearchLogger.pointerTracker_onDownEvent(deltaT, distance * distance);
}
- cancelTracking();
+ cancelTrackingForAction();
return;
}
}
@@ -887,7 +899,7 @@ public final class PointerTracker implements PointerTrackerQueue.Element {
|| (key != null && key.isModifier())
|| mKeyDetector.alwaysAllowsSlidingInput();
mKeyboardLayoutHasBeenChanged = false;
- mIsTrackingCanceled = false;
+ mIsTrackingForActionDisabled = false;
resetSlidingKeyInput();
if (key != null) {
// This onPress call may have changed keyboard layout. Those cases are detected at
@@ -947,7 +959,7 @@ public final class PointerTracker implements PointerTrackerQueue.Element {
if (DEBUG_MOVE_EVENT) {
printTouchEvent("onMoveEvent:", x, y, eventTime);
}
- if (mIsTrackingCanceled) {
+ if (mIsTrackingForActionDisabled) {
return;
}
@@ -985,6 +997,9 @@ public final class PointerTracker implements PointerTrackerQueue.Element {
key = onMoveKey(x, y);
}
onMoveToNewKey(key, x, y);
+ if (mIsTrackingForActionDisabled) {
+ return;
+ }
startLongPressTimer(key);
setPressedKeyGraphics(key, eventTime);
}
@@ -1028,7 +1043,7 @@ public final class PointerTracker implements PointerTrackerQueue.Element {
private void processSildeOutFromOldKey(final Key oldKey) {
setReleasedKeyGraphics(oldKey);
- callListenerOnRelease(oldKey, oldKey.mCode, true);
+ callListenerOnRelease(oldKey, oldKey.mCode, true /* withSliding */);
startSlidingKeyInput(oldKey);
mTimerProxy.cancelKeyTimers();
}
@@ -1069,11 +1084,11 @@ public final class PointerTracker implements PointerTrackerQueue.Element {
+ " detected sliding finger while multi touching", mPointerId));
}
onUpEvent(x, y, eventTime);
- cancelTracking();
+ cancelTrackingForAction();
setReleasedKeyGraphics(oldKey);
} else {
if (!mIsDetectingGesture) {
- cancelTracking();
+ cancelTrackingForAction();
}
setReleasedKeyGraphics(oldKey);
}
@@ -1087,7 +1102,7 @@ public final class PointerTracker implements PointerTrackerQueue.Element {
onMoveToNewKey(null, x, y);
} else {
if (!mIsDetectingGesture) {
- cancelTracking();
+ cancelTrackingForAction();
}
}
}
@@ -1155,11 +1170,13 @@ public final class PointerTracker implements PointerTrackerQueue.Element {
return;
}
onUpEventInternal(mLastX, mLastY, eventTime);
- cancelTracking();
+ cancelTrackingForAction();
}
private void onUpEventInternal(final int x, final int y, final long eventTime) {
mTimerProxy.cancelKeyTimers();
+ final boolean isInSlidingKeyInput = mIsInSlidingKeyInput;
+ final boolean isInSlidingKeyInputFromModifier = mIsInSlidingKeyInputFromModifier;
resetSlidingKeyInput();
mIsDetectingGesture = false;
final Key currentKey = mCurrentKey;
@@ -1168,7 +1185,7 @@ public final class PointerTracker implements PointerTrackerQueue.Element {
setReleasedKeyGraphics(currentKey);
if (isShowingMoreKeysPanel()) {
- if (!mIsTrackingCanceled) {
+ if (!mIsTrackingForActionDisabled) {
final int translatedX = mMoreKeysPanel.translateX(x);
final int translatedY = mMoreKeysPanel.translateY(y);
mMoreKeysPanel.onUpEvent(translatedX, translatedY, mPointerId, eventTime);
@@ -1180,17 +1197,22 @@ public final class PointerTracker implements PointerTrackerQueue.Element {
if (sInGesture) {
if (currentKey != null) {
- callListenerOnRelease(currentKey, currentKey.mCode, true);
+ callListenerOnRelease(currentKey, currentKey.mCode, true /* withSliding */);
}
mayEndBatchInput(eventTime);
return;
}
- if (mIsTrackingCanceled) {
+ if (mIsTrackingForActionDisabled) {
+ return;
+ }
+ if (currentKey != null && currentKey.isRepeatable() && !isInSlidingKeyInput) {
+ // Repeatable key has been registered in {@link #onDownEventInternal(int,int,long)}.
return;
}
- if (currentKey != null && !currentKey.isRepeatable()) {
- detectAndSendKey(currentKey, mKeyX, mKeyY, eventTime);
+ detectAndSendKey(currentKey, mKeyX, mKeyY, eventTime);
+ if (isInSlidingKeyInputFromModifier) {
+ callListenerOnFinishSlidingInput();
}
}
@@ -1203,16 +1225,16 @@ public final class PointerTracker implements PointerTrackerQueue.Element {
}
@Override
- public void cancelTracking() {
+ public void cancelTrackingForAction() {
if (isShowingMoreKeysPanel()) {
return;
}
- mIsTrackingCanceled = true;
+ mIsTrackingForActionDisabled = true;
}
public void onLongPressed() {
resetSlidingKeyInput();
- cancelTracking();
+ cancelTrackingForAction();
setReleasedKeyGraphics(mCurrentKey);
sPointerTrackerQueue.remove(this);
}
@@ -1239,10 +1261,13 @@ public final class PointerTracker implements PointerTrackerQueue.Element {
}
private void startRepeatKey(final Key key) {
- if (key != null && key.isRepeatable() && !sInGesture) {
- onRegisterKey(key);
- mTimerProxy.startKeyRepeatTimer(this);
- }
+ if (sInGesture) return;
+ if (key == null) return;
+ if (!key.isRepeatable()) return;
+ // Don't start key repeat when we are in sliding input mode.
+ if (mIsInSlidingKeyInput) return;
+ onRegisterKey(key);
+ mTimerProxy.startKeyRepeatTimer(this);
}
public void onRegisterKey(final Key key) {
@@ -1295,9 +1320,15 @@ public final class PointerTracker implements PointerTrackerQueue.Element {
}
private void startLongPressTimer(final Key key) {
- if (key != null && key.isLongPressEnabled() && !sInGesture) {
- mTimerProxy.startLongPressTimer(this);
- }
+ if (sInGesture) return;
+ if (key == null) return;
+ if (!key.isLongPressEnabled()) return;
+ // Caveat: Please note that isLongPressEnabled() can be true even if the current key
+ // doesn't have its more keys. (e.g. spacebar, globe key)
+ // We always need to start the long press timer if the key has its more keys regardless of
+ // whether or not we are in the sliding input mode.
+ if (mIsInSlidingKeyInput && key.mMoreKeys == null) return;
+ mTimerProxy.startLongPressTimer(this);
}
private void detectAndSendKey(final Key key, final int x, final int y, final long eventTime) {
@@ -1308,7 +1339,7 @@ public final class PointerTracker implements PointerTrackerQueue.Element {
final int code = key.mCode;
callListenerOnCodeInput(key, code, x, y, eventTime);
- callListenerOnRelease(key, code, false);
+ callListenerOnRelease(key, code, false /* withSliding */);
}
private void printTouchEvent(final String title, final int x, final int y,
@@ -1316,6 +1347,6 @@ public final class PointerTracker implements PointerTrackerQueue.Element {
final Key key = mKeyDetector.detectHitKey(x, y);
final String code = KeyDetector.printableCode(key);
Log.d(TAG, String.format("[%d]%s%s %4d %4d %5d %s", mPointerId,
- (mIsTrackingCanceled ? "-" : " "), title, x, y, eventTime, code));
+ (mIsTrackingForActionDisabled ? "-" : " "), title, x, y, eventTime, code));
}
}
diff --git a/java/src/com/android/inputmethod/keyboard/ProximityInfo.java b/java/src/com/android/inputmethod/keyboard/ProximityInfo.java
index b77e378bf..57d3fede4 100644
--- a/java/src/com/android/inputmethod/keyboard/ProximityInfo.java
+++ b/java/src/com/android/inputmethod/keyboard/ProximityInfo.java
@@ -79,23 +79,6 @@ public class ProximityInfo {
mNativeProximityInfo = createNativeProximityInfo(touchPositionCorrection);
}
- /**
- * Constructor for subclasses such as
- * {@link com.android.inputmethod.latin.spellcheck.SpellCheckerProximityInfo}.
- */
- protected ProximityInfo(final int[] proximityCharsArray, final int gridWidth,
- final int gridHeight) {
- this("", 1, 1, 1, 1, 1, 1, EMPTY_KEY_ARRAY, null);
- mNativeProximityInfo = setProximityInfoNative("" /* locale */,
- gridWidth /* displayWidth */, gridHeight /* displayHeight */,
- gridWidth, gridHeight, 1 /* mostCommonKeyWidth */,
- 1 /* mostCommonKeyHeight */, proximityCharsArray, 0 /* keyCount */,
- null /*keyXCoordinates */, null /* keyYCoordinates */,
- null /* keyWidths */, null /* keyHeights */, null /* keyCharCodes */,
- null /* sweetSpotCenterXs */, null /* sweetSpotCenterYs */,
- null /* sweetSpotRadii */);
- }
-
private long mNativeProximityInfo;
static {
JniUtils.loadNativeLibrary();
diff --git a/java/src/com/android/inputmethod/keyboard/internal/GestureStroke.java b/java/src/com/android/inputmethod/keyboard/internal/GestureStroke.java
index 53da47c52..70363e602 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/GestureStroke.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/GestureStroke.java
@@ -83,9 +83,8 @@ public class GestureStroke {
public final int mRecognitionMinimumTime; // msec
public final float mRecognitionSpeedThreshold; // keyWidth/sec
- // Default GestureStroke parameters for test.
- public static final GestureStrokeParams FOR_TEST = new GestureStrokeParams();
- public static final GestureStrokeParams DEFAULT = FOR_TEST;
+ // Default GestureStroke parameters.
+ public static final GestureStrokeParams DEFAULT = new GestureStrokeParams();
private GestureStrokeParams() {
// These parameter values are default and intended for testing.
@@ -146,7 +145,7 @@ public class GestureStroke {
public void setKeyboardGeometry(final int keyWidth, final int keyboardHeight) {
mKeyWidth = keyWidth;
mMinYCoordinate = -(int)(keyboardHeight * EXTRA_GESTURE_TRAIL_AREA_ABOVE_KEYBOARD_RATIO);
- mMaxYCoordinate = keyboardHeight - 1;
+ mMaxYCoordinate = keyboardHeight;
// TODO: Find an appropriate base metric for these length. Maybe diagonal length of the key?
mDetectFastMoveSpeedThreshold = (int)(keyWidth * mParams.mDetectFastMoveSpeedThreshold);
mGestureDynamicDistanceThresholdFrom =
diff --git a/java/src/com/android/inputmethod/keyboard/internal/GestureStrokeWithPreviewPoints.java b/java/src/com/android/inputmethod/keyboard/internal/GestureStrokeWithPreviewPoints.java
index 3315954c1..b31f00b62 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/GestureStrokeWithPreviewPoints.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/GestureStrokeWithPreviewPoints.java
@@ -16,40 +16,74 @@
package com.android.inputmethod.keyboard.internal;
+import android.content.res.TypedArray;
+
+import com.android.inputmethod.latin.R;
import com.android.inputmethod.latin.ResizableIntArray;
public final class GestureStrokeWithPreviewPoints extends GestureStroke {
public static final int PREVIEW_CAPACITY = 256;
- private static final boolean ENABLE_INTERPOLATION = true;
-
private final ResizableIntArray mPreviewEventTimes = new ResizableIntArray(PREVIEW_CAPACITY);
private final ResizableIntArray mPreviewXCoordinates = new ResizableIntArray(PREVIEW_CAPACITY);
private final ResizableIntArray mPreviewYCoordinates = new ResizableIntArray(PREVIEW_CAPACITY);
+ private final GestureStrokePreviewParams mPreviewParams;
+
private int mStrokeId;
private int mLastPreviewSize;
private final HermiteInterpolator mInterpolator = new HermiteInterpolator();
private int mLastInterpolatedPreviewIndex;
- private int mMinPreviewSamplingDistanceSquared;
private int mLastX;
private int mLastY;
- private double mMinPreviewSamplingDistance;
private double mDistanceFromLastSample;
- // TODO: Move these constants to resource.
- // The minimum linear distance between sample points for preview in keyWidth unit.
- private static final float MIN_PREVIEW_SAMPLING_RATIO_TO_KEY_WIDTH = 0.1f;
- // The minimum trail distance between sample points for preview in keyWidth unit when using
- // interpolation.
- private static final float MIN_PREVIEW_SAMPLING_RATIO_TO_KEY_WIDTH_WITH_INTERPOLATION = 0.2f;
- // The angular threshold to use interpolation in radian. PI/12 is 15 degree.
- private static final double INTERPOLATION_ANGULAR_THRESHOLD = Math.PI / 12.0d;
- private static final int MAX_INTERPOLATION_PARTITION = 4;
-
- public GestureStrokeWithPreviewPoints(final int pointerId, final GestureStrokeParams params) {
- super(pointerId, params);
+ public static final class GestureStrokePreviewParams {
+ public final double mMinSamplingDistance; // in pixel
+ public final double mMaxInterpolationAngularThreshold; // in radian
+ public final double mMaxInterpolationDistanceThreshold; // in pixel
+ public final int mMaxInterpolationSegments;
+
+ public static final GestureStrokePreviewParams DEFAULT = new GestureStrokePreviewParams();
+
+ private static final int DEFAULT_MAX_INTERPOLATION_ANGULAR_THRESHOLD = 15; // in degree
+
+ private GestureStrokePreviewParams() {
+ mMinSamplingDistance = 0.0d;
+ mMaxInterpolationAngularThreshold =
+ degreeToRadian(DEFAULT_MAX_INTERPOLATION_ANGULAR_THRESHOLD);
+ mMaxInterpolationDistanceThreshold = mMinSamplingDistance;
+ mMaxInterpolationSegments = 4;
+ }
+
+ private static double degreeToRadian(final int degree) {
+ return (double)degree / 180.0d * Math.PI;
+ }
+
+ public GestureStrokePreviewParams(final TypedArray mainKeyboardViewAttr) {
+ mMinSamplingDistance = mainKeyboardViewAttr.getDimension(
+ R.styleable.MainKeyboardView_gestureTrailMinSamplingDistance,
+ (float)DEFAULT.mMinSamplingDistance);
+ final int interpolationAngularDegree = mainKeyboardViewAttr.getInteger(R.styleable
+ .MainKeyboardView_gestureTrailMaxInterpolationAngularThreshold, 0);
+ mMaxInterpolationAngularThreshold = (interpolationAngularDegree <= 0)
+ ? DEFAULT.mMaxInterpolationAngularThreshold
+ : degreeToRadian(interpolationAngularDegree);
+ mMaxInterpolationDistanceThreshold = mainKeyboardViewAttr.getDimension(R.styleable
+ .MainKeyboardView_gestureTrailMaxInterpolationDistanceThreshold,
+ (float)DEFAULT.mMaxInterpolationDistanceThreshold);
+ mMaxInterpolationSegments = mainKeyboardViewAttr.getInteger(
+ R.styleable.MainKeyboardView_gestureTrailMaxInterpolationSegments,
+ DEFAULT.mMaxInterpolationSegments);
+ }
+ }
+
+ public GestureStrokeWithPreviewPoints(final int pointerId,
+ final GestureStrokeParams strokeParams,
+ final GestureStrokePreviewParams previewParams) {
+ super(pointerId, strokeParams);
+ mPreviewParams = previewParams;
}
@Override
@@ -67,34 +101,13 @@ public final class GestureStrokeWithPreviewPoints extends GestureStroke {
return mStrokeId;
}
- @Override
- public void setKeyboardGeometry(final int keyWidth, final int keyboardHeight) {
- super.setKeyboardGeometry(keyWidth, keyboardHeight);
- final float samplingRatioToKeyWidth = ENABLE_INTERPOLATION
- ? MIN_PREVIEW_SAMPLING_RATIO_TO_KEY_WIDTH_WITH_INTERPOLATION
- : MIN_PREVIEW_SAMPLING_RATIO_TO_KEY_WIDTH;
- mMinPreviewSamplingDistance = keyWidth * samplingRatioToKeyWidth;
- mMinPreviewSamplingDistanceSquared = (int)(
- mMinPreviewSamplingDistance * mMinPreviewSamplingDistance);
- }
-
- private boolean needsSampling(final int x, final int y, final boolean isMajorEvent) {
- if (ENABLE_INTERPOLATION) {
- mDistanceFromLastSample += Math.hypot(x - mLastX, y - mLastY);
- mLastX = x;
- mLastY = y;
- if (mDistanceFromLastSample >= mMinPreviewSamplingDistance) {
- mDistanceFromLastSample = 0.0d;
- return true;
- }
- return false;
- }
-
- final int dx = x - mLastX;
- final int dy = y - mLastY;
- if (isMajorEvent || dx * dx + dy * dy >= mMinPreviewSamplingDistanceSquared) {
- mLastX = x;
- mLastY = y;
+ private boolean needsSampling(final int x, final int y) {
+ mDistanceFromLastSample += Math.hypot(x - mLastX, y - mLastY);
+ mLastX = x;
+ mLastY = y;
+ final boolean isDownEvent = (mPreviewEventTimes.getLength() == 0);
+ if (mDistanceFromLastSample >= mPreviewParams.mMinSamplingDistance || isDownEvent) {
+ mDistanceFromLastSample = 0.0d;
return true;
}
return false;
@@ -103,7 +116,7 @@ public final class GestureStrokeWithPreviewPoints extends GestureStroke {
@Override
public boolean addPointOnKeyboard(final int x, final int y, final int time,
final boolean isMajorEvent) {
- if (needsSampling(x, y, isMajorEvent)) {
+ if (needsSampling(x, y)) {
mPreviewEventTimes.add(time);
mPreviewXCoordinates.add(x);
mPreviewYCoordinates.add(y);
@@ -132,17 +145,14 @@ public final class GestureStrokeWithPreviewPoints extends GestureStroke {
*
* @param lastInterpolatedIndex the start index of the last interpolated segment of
* <code>eventTimes</code>, <code>xCoords</code>, and <code>yCoords</code>.
- * @param eventTimes the event time array of gesture preview trail to be drawn.
- * @param xCoords the x-coordinates array of gesture preview trail to be drawn.
- * @param yCoords the y-coordinates array of gesture preview trail to be drawn.
+ * @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.
* @return the start index of the last interpolated segment of input arrays.
*/
public int interpolateStrokeAndReturnStartIndexOfLastSegment(final int lastInterpolatedIndex,
final ResizableIntArray eventTimes, final ResizableIntArray xCoords,
- final ResizableIntArray yCoords) {
- if (!ENABLE_INTERPOLATION) {
- return lastInterpolatedIndex;
- }
+ final ResizableIntArray yCoords, final ResizableIntArray types) {
final int size = mPreviewEventTimes.getLength();
final int[] pt = mPreviewEventTimes.getPrimitiveArray();
final int[] px = mPreviewXCoordinates.getPrimitiveArray();
@@ -161,23 +171,35 @@ public final class GestureStrokeWithPreviewPoints extends GestureStroke {
mInterpolator.setInterval(p0, p1, p2, p3);
final double m1 = Math.atan2(mInterpolator.mSlope1Y, mInterpolator.mSlope1X);
final double m2 = Math.atan2(mInterpolator.mSlope2Y, mInterpolator.mSlope2X);
- final double dm = Math.abs(angularDiff(m2, m1));
- final int partition = Math.min((int)Math.ceil(dm / INTERPOLATION_ANGULAR_THRESHOLD),
- MAX_INTERPOLATION_PARTITION);
+ final double deltaAngle = Math.abs(angularDiff(m2, m1));
+ final int segmentsByAngle = (int)Math.ceil(
+ deltaAngle / mPreviewParams.mMaxInterpolationAngularThreshold);
+ final double deltaDistance = Math.hypot(mInterpolator.mP1X - mInterpolator.mP2X,
+ mInterpolator.mP1Y - mInterpolator.mP2Y);
+ final int segmentsByDistance = (int)Math.ceil(deltaDistance
+ / mPreviewParams.mMaxInterpolationDistanceThreshold);
+ final int segments = Math.min(mPreviewParams.mMaxInterpolationSegments,
+ Math.max(segmentsByAngle, segmentsByDistance));
final int t1 = eventTimes.get(d1);
final int dt = pt[p2] - pt[p1];
d1++;
- for (int i = 1; i < partition; i++) {
- final float t = i / (float)partition;
+ for (int i = 1; i < segments; i++) {
+ final float t = i / (float)segments;
mInterpolator.interpolate(t);
eventTimes.add(d1, (int)(dt * t) + t1);
xCoords.add(d1, (int)mInterpolator.mInterpolatedX);
yCoords.add(d1, (int)mInterpolator.mInterpolatedY);
+ if (GestureTrail.DBG_SHOW_POINTS) {
+ types.add(d1, GestureTrail.POINT_TYPE_INTERPOLATED);
+ }
d1++;
}
eventTimes.add(d1, pt[p2]);
xCoords.add(d1, px[p2]);
yCoords.add(d1, py[p2]);
+ if (GestureTrail.DBG_SHOW_POINTS) {
+ types.add(d1, GestureTrail.POINT_TYPE_SAMPLED);
+ }
}
return lastInterpolatedDrawIndex;
}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/GesturePreviewTrail.java b/java/src/com/android/inputmethod/keyboard/internal/GestureTrail.java
index 7fd1bedcb..03dd1c372 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/GesturePreviewTrail.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/GestureTrail.java
@@ -18,6 +18,7 @@ package com.android.inputmethod.keyboard.internal;
import android.content.res.TypedArray;
import android.graphics.Canvas;
+import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Rect;
@@ -28,19 +29,26 @@ import com.android.inputmethod.latin.R;
import com.android.inputmethod.latin.ResizableIntArray;
/*
- * @attr ref R.styleable#MainKeyboardView_gesturePreviewTrailFadeoutStartDelay
- * @attr ref R.styleable#MainKeyboardView_gesturePreviewTrailFadeoutDuration
- * @attr ref R.styleable#MainKeyboardView_gesturePreviewTrailUpdateInterval
- * @attr ref R.styleable#MainKeyboardView_gesturePreviewTrailColor
- * @attr ref R.styleable#MainKeyboardView_gesturePreviewTrailWidth
+ * @attr ref R.styleable#MainKeyboardView_gestureTrailFadeoutStartDelay
+ * @attr ref R.styleable#MainKeyboardView_gestureTrailFadeoutDuration
+ * @attr ref R.styleable#MainKeyboardView_gestureTrailUpdateInterval
+ * @attr ref R.styleable#MainKeyboardView_gestureTrailColor
+ * @attr ref R.styleable#MainKeyboardView_gestureTrailWidth
*/
-final class GesturePreviewTrail {
+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;
+
private static final int DEFAULT_CAPACITY = GestureStrokeWithPreviewPoints.PREVIEW_CAPACITY;
// These three {@link ResizableIntArray}s should be synchronized by {@link #mEventTimes}.
private final ResizableIntArray mXCoordinates = new ResizableIntArray(DEFAULT_CAPACITY);
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);
private int mCurrentStrokeId = -1;
// The wall time of the zero value in {@link #mEventTimes}
private long mCurrentTimeBase;
@@ -62,26 +70,26 @@ final class GesturePreviewTrail {
public Params(final TypedArray mainKeyboardViewAttr) {
mTrailColor = mainKeyboardViewAttr.getColor(
- R.styleable.MainKeyboardView_gesturePreviewTrailColor, 0);
+ R.styleable.MainKeyboardView_gestureTrailColor, 0);
mTrailStartWidth = mainKeyboardViewAttr.getDimension(
- R.styleable.MainKeyboardView_gesturePreviewTrailStartWidth, 0.0f);
+ R.styleable.MainKeyboardView_gestureTrailStartWidth, 0.0f);
mTrailEndWidth = mainKeyboardViewAttr.getDimension(
- R.styleable.MainKeyboardView_gesturePreviewTrailEndWidth, 0.0f);
+ R.styleable.MainKeyboardView_gestureTrailEndWidth, 0.0f);
final int PERCENTAGE_INT = 100;
mTrailBodyRatio = (float)mainKeyboardViewAttr.getInt(
- R.styleable.MainKeyboardView_gesturePreviewTrailBodyRatio, PERCENTAGE_INT)
+ R.styleable.MainKeyboardView_gestureTrailBodyRatio, PERCENTAGE_INT)
/ (float)PERCENTAGE_INT;
final int trailShadowRatioInt = mainKeyboardViewAttr.getInt(
- R.styleable.MainKeyboardView_gesturePreviewTrailShadowRatio, 0);
+ R.styleable.MainKeyboardView_gestureTrailShadowRatio, 0);
mTrailShadowEnabled = (trailShadowRatioInt > 0);
mTrailShadowRatio = (float)trailShadowRatioInt / (float)PERCENTAGE_INT;
- mFadeoutStartDelay = mainKeyboardViewAttr.getInt(
- R.styleable.MainKeyboardView_gesturePreviewTrailFadeoutStartDelay, 0);
- mFadeoutDuration = mainKeyboardViewAttr.getInt(
- R.styleable.MainKeyboardView_gesturePreviewTrailFadeoutDuration, 0);
+ mFadeoutStartDelay = DBG_SHOW_POINTS ? 2000 : mainKeyboardViewAttr.getInt(
+ R.styleable.MainKeyboardView_gestureTrailFadeoutStartDelay, 0);
+ mFadeoutDuration = DBG_SHOW_POINTS ? 200 : mainKeyboardViewAttr.getInt(
+ R.styleable.MainKeyboardView_gestureTrailFadeoutDuration, 0);
mTrailLingerDuration = mFadeoutStartDelay + mFadeoutDuration;
mUpdateInterval = mainKeyboardViewAttr.getInt(
- R.styleable.MainKeyboardView_gesturePreviewTrailUpdateInterval, 0);
+ R.styleable.MainKeyboardView_gestureTrailUpdateInterval, 0);
}
}
@@ -125,7 +133,7 @@ final class GesturePreviewTrail {
final int lastInterpolatedIndex = (strokeId == mCurrentStrokeId)
? mLastInterpolatedDrawIndex : trailSize;
mLastInterpolatedDrawIndex = stroke.interpolateStrokeAndReturnStartIndexOfLastSegment(
- lastInterpolatedIndex, mEventTimes, mXCoordinates, mYCoordinates);
+ lastInterpolatedIndex, mEventTimes, mXCoordinates, mYCoordinates, mPointTypes);
if (strokeId != mCurrentStrokeId) {
final int elapsedTime = (int)(downTime - mCurrentTimeBase);
for (int i = mTrailStartIndex; i < trailSize; i++) {
@@ -178,12 +186,12 @@ final class GesturePreviewTrail {
private final Rect mRoundedLineBounds = new Rect();
/**
- * Draw gesture preview trail
- * @param canvas The canvas to draw the gesture preview trail
- * @param paint The paint object to be used to draw the gesture preview trail
+ * Draw gesture trail
+ * @param canvas The canvas to draw the gesture trail
+ * @param paint The paint object to be used to draw the gesture trail
* @param outBoundsRect the bounding box of this gesture trail drawing
- * @param params The drawing parameters of gesture preview trail
- * @return true if some gesture preview trails remain to be drawn
+ * @param params The drawing parameters of gesture trail
+ * @return true if some gesture trails remain to be drawn
*/
public boolean drawGestureTrail(final Canvas canvas, final Paint paint,
final Rect outBoundsRect, final Params params) {
@@ -204,6 +212,7 @@ final class GesturePreviewTrail {
final int[] eventTimes = mEventTimes.getPrimitiveArray();
final int[] xCoords = mXCoordinates.getPrimitiveArray();
final int[] yCoords = mYCoordinates.getPrimitiveArray();
+ final int[] pointTypes = mPointTypes.getPrimitiveArray();
final int sinceDown = (int)(SystemClock.uptimeMillis() - mCurrentTimeBase);
int startIndex;
for (startIndex = mTrailStartIndex; startIndex < trailSize; startIndex++) {
@@ -246,6 +255,17 @@ final class GesturePreviewTrail {
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;
@@ -265,6 +285,9 @@ final class GesturePreviewTrail {
mEventTimes.setLength(newSize);
mXCoordinates.setLength(newSize);
mYCoordinates.setLength(newSize);
+ if (DBG_SHOW_POINTS) {
+ mPointTypes.setLength(newSize);
+ }
// The start index of the last segment of the stroke
// {@link mLastInterpolatedDrawIndex} should also be updated because all array
// elements have just been shifted for compaction or been zeroed.
diff --git a/java/src/com/android/inputmethod/keyboard/internal/GestureTrailsPreview.java b/java/src/com/android/inputmethod/keyboard/internal/GestureTrailsPreview.java
index 85558f1f6..1e4c43ee5 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/GestureTrailsPreview.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/GestureTrailsPreview.java
@@ -29,7 +29,7 @@ import android.util.SparseArray;
import android.view.View;
import com.android.inputmethod.keyboard.PointerTracker;
-import com.android.inputmethod.keyboard.internal.GesturePreviewTrail.Params;
+import com.android.inputmethod.keyboard.internal.GestureTrail.Params;
import com.android.inputmethod.latin.CollectionUtils;
import com.android.inputmethod.latin.StaticInnerHandlerWrapper;
@@ -37,9 +37,8 @@ import com.android.inputmethod.latin.StaticInnerHandlerWrapper;
* Draw gesture trail preview graphics during gesture.
*/
public final class GestureTrailsPreview extends AbstractDrawingPreview {
- private final SparseArray<GesturePreviewTrail> mGesturePreviewTrails =
- CollectionUtils.newSparseArray();
- private final Params mGesturePreviewTrailParams;
+ private final SparseArray<GestureTrail> mGestureTrails = CollectionUtils.newSparseArray();
+ private final Params mGestureTrailParams;
private final Paint mGesturePaint;
private int mOffscreenWidth;
private int mOffscreenHeight;
@@ -48,20 +47,20 @@ public final class GestureTrailsPreview extends AbstractDrawingPreview {
private final Canvas mOffscreenCanvas = new Canvas();
private final Rect mOffscreenSrcRect = new Rect();
private final Rect mDirtyRect = new Rect();
- private final Rect mGesturePreviewTrailBoundsRect = new Rect(); // per trail
+ private final Rect mGestureTrailBoundsRect = new Rect(); // per trail
private final DrawingHandler mDrawingHandler;
private static final class DrawingHandler
extends StaticInnerHandlerWrapper<GestureTrailsPreview> {
- private static final int MSG_UPDATE_GESTURE_PREVIEW_TRAIL = 0;
+ private static final int MSG_UPDATE_GESTURE_TRAIL = 0;
- private final Params mGesturePreviewTrailParams;
+ private final Params mGestureTrailParams;
public DrawingHandler(final GestureTrailsPreview outerInstance,
- final Params gesturePreviewTrailParams) {
+ final Params gestureTrailParams) {
super(outerInstance);
- mGesturePreviewTrailParams = gesturePreviewTrailParams;
+ mGestureTrailParams = gestureTrailParams;
}
@Override
@@ -69,23 +68,23 @@ public final class GestureTrailsPreview extends AbstractDrawingPreview {
final GestureTrailsPreview preview = getOuterInstance();
if (preview == null) return;
switch (msg.what) {
- case MSG_UPDATE_GESTURE_PREVIEW_TRAIL:
+ case MSG_UPDATE_GESTURE_TRAIL:
preview.getDrawingView().invalidate();
break;
}
}
public void postUpdateGestureTrailPreview() {
- removeMessages(MSG_UPDATE_GESTURE_PREVIEW_TRAIL);
- sendMessageDelayed(obtainMessage(MSG_UPDATE_GESTURE_PREVIEW_TRAIL),
- mGesturePreviewTrailParams.mUpdateInterval);
+ removeMessages(MSG_UPDATE_GESTURE_TRAIL);
+ sendMessageDelayed(obtainMessage(MSG_UPDATE_GESTURE_TRAIL),
+ mGestureTrailParams.mUpdateInterval);
}
}
public GestureTrailsPreview(final View drawingView, final TypedArray mainKeyboardViewAttr) {
super(drawingView);
- mGesturePreviewTrailParams = new Params(mainKeyboardViewAttr);
- mDrawingHandler = new DrawingHandler(this, mGesturePreviewTrailParams);
+ mGestureTrailParams = new Params(mainKeyboardViewAttr);
+ mDrawingHandler = new DrawingHandler(this, mGestureTrailParams);
final Paint gesturePaint = new Paint();
gesturePaint.setAntiAlias(true);
gesturePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC));
@@ -133,21 +132,20 @@ public final class GestureTrailsPreview extends AbstractDrawingPreview {
offscreenCanvas.drawRect(dirtyRect, paint);
}
dirtyRect.setEmpty();
- boolean needsUpdatingGesturePreviewTrail = false;
+ boolean needsUpdatingGestureTrail = false;
// Draw gesture trails to offscreen buffer.
- synchronized (mGesturePreviewTrails) {
+ synchronized (mGestureTrails) {
// Trails count == fingers count that have ever been active.
- final int trailsCount = mGesturePreviewTrails.size();
+ final int trailsCount = mGestureTrails.size();
for (int index = 0; index < trailsCount; index++) {
- final GesturePreviewTrail trail = mGesturePreviewTrails.valueAt(index);
- needsUpdatingGesturePreviewTrail |=
- trail.drawGestureTrail(offscreenCanvas, paint,
- mGesturePreviewTrailBoundsRect, mGesturePreviewTrailParams);
- // {@link #mGesturePreviewTrailBoundsRect} has bounding box of the trail.
- dirtyRect.union(mGesturePreviewTrailBoundsRect);
+ final GestureTrail trail = mGestureTrails.valueAt(index);
+ needsUpdatingGestureTrail |= trail.drawGestureTrail(offscreenCanvas, paint,
+ mGestureTrailBoundsRect, mGestureTrailParams);
+ // {@link #mGestureTrailBoundsRect} has bounding box of the trail.
+ dirtyRect.union(mGestureTrailBoundsRect);
}
}
- return needsUpdatingGesturePreviewTrail;
+ return needsUpdatingGestureTrail;
}
/**
@@ -161,9 +159,9 @@ public final class GestureTrailsPreview extends AbstractDrawingPreview {
}
mayAllocateOffscreenBuffer();
// Draw gesture trails to offscreen buffer.
- final boolean needsUpdatingGesturePreviewTrail = drawGestureTrails(
+ final boolean needsUpdatingGestureTrail = drawGestureTrails(
mOffscreenCanvas, mGesturePaint, mDirtyRect);
- if (needsUpdatingGesturePreviewTrail) {
+ if (needsUpdatingGestureTrail) {
mDrawingHandler.postUpdateGestureTrailPreview();
}
// Transfer offscreen buffer to screen.
@@ -185,12 +183,12 @@ public final class GestureTrailsPreview extends AbstractDrawingPreview {
if (!isPreviewEnabled()) {
return;
}
- GesturePreviewTrail trail;
- synchronized (mGesturePreviewTrails) {
- trail = mGesturePreviewTrails.get(tracker.mPointerId);
+ GestureTrail trail;
+ synchronized (mGestureTrails) {
+ trail = mGestureTrails.get(tracker.mPointerId);
if (trail == null) {
- trail = new GesturePreviewTrail();
- mGesturePreviewTrails.put(tracker.mPointerId, trail);
+ trail = new GestureTrail();
+ mGestureTrails.put(tracker.mPointerId, trail);
}
}
trail.addStroke(tracker.getGestureStrokeWithPreviewPoints(), tracker.getDownTime());
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardState.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardState.java
index b1d499702..6af1bd75f 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardState.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardState.java
@@ -28,9 +28,9 @@ import com.android.inputmethod.latin.RecapitalizeStatus;
* This class contains all keyboard state transition logic.
*
* The input events are {@link #onLoadKeyboard()}, {@link #onSaveKeyboardState()},
- * {@link #onPressKey(int, boolean, int)}, {@link #onReleaseKey(int, boolean)},
- * {@link #onCodeInput(int, boolean, int)}, {@link #onCancelInput(boolean)},
- * {@link #onUpdateShiftState(int, int)}, {@link #onLongPressTimeout(int)}.
+ * {@link #onPressKey(int,boolean,int)}, {@link #onReleaseKey(int,boolean)},
+ * {@link #onCodeInput(int,int)}, {@link #onFinishSlidingInput()}, {@link #onCancelInput()},
+ * {@link #onUpdateShiftState(int,int)}, {@link #onLongPressTimeout(int)}.
*
* The actions are {@link SwitchActions}'s methods.
*/
@@ -74,6 +74,7 @@ public final class KeyboardState {
private static final int SWITCH_STATE_SYMBOL = 2;
private static final int SWITCH_STATE_MOMENTARY_ALPHA_AND_SYMBOL = 3;
private static final int SWITCH_STATE_MOMENTARY_SYMBOL_AND_MORE = 4;
+ private static final int SWITCH_STATE_MOMENTARY_ALPHA_SHIFT = 5;
private int mSwitchState = SWITCH_STATE_ALPHA;
private boolean mIsAlphabetMode;
@@ -96,16 +97,16 @@ public final class KeyboardState {
public boolean mIsValid;
public boolean mIsAlphabetMode;
public boolean mIsAlphabetShiftLocked;
- public boolean mIsShifted;
+ public int mShiftMode;
@Override
public String toString() {
if (!mIsValid) return "INVALID";
if (mIsAlphabetMode) {
if (mIsAlphabetShiftLocked) return "ALPHABET_SHIFT_LOCKED";
- return mIsShifted ? "ALPHABET_SHIFTED" : "ALPHABET";
+ return "ALPHABET_" + shiftModeToString(mShiftMode);
} else {
- return mIsShifted ? "SYMBOLS_SHIFTED" : "SYMBOLS";
+ return "SYMBOLS_" + shiftModeToString(mShiftMode);
}
}
}
@@ -128,16 +129,21 @@ public final class KeyboardState {
onRestoreKeyboardState();
}
+ private static final int UNSHIFT = 0;
+ private static final int MANUAL_SHIFT = 1;
+ private static final int AUTOMATIC_SHIFT = 2;
+ private static final int SHIFT_LOCK_SHIFTED = 3;
+
public void onSaveKeyboardState() {
final SavedKeyboardState state = mSavedKeyboardState;
state.mIsAlphabetMode = mIsAlphabetMode;
if (mIsAlphabetMode) {
state.mIsAlphabetShiftLocked = mAlphabetShiftState.isShiftLocked();
- state.mIsShifted = !state.mIsAlphabetShiftLocked
- && mAlphabetShiftState.isShiftedOrShiftLocked();
+ state.mShiftMode = mAlphabetShiftState.isAutomaticShifted() ? AUTOMATIC_SHIFT
+ : (mAlphabetShiftState.isShiftedOrShiftLocked() ? MANUAL_SHIFT : UNSHIFT);
} else {
state.mIsAlphabetShiftLocked = mPrevMainKeyboardWasShiftLocked;
- state.mIsShifted = mIsSymbolShifted;
+ state.mShiftMode = mIsSymbolShifted ? MANUAL_SHIFT : UNSHIFT;
}
state.mIsValid = true;
if (DEBUG_EVENT) {
@@ -153,7 +159,7 @@ public final class KeyboardState {
if (!state.mIsValid || state.mIsAlphabetMode) {
setAlphabetKeyboard();
} else {
- if (state.mIsShifted) {
+ if (state.mShiftMode == MANUAL_SHIFT) {
setSymbolsShiftedKeyboard();
} else {
setSymbolsKeyboard();
@@ -166,18 +172,13 @@ public final class KeyboardState {
if (state.mIsAlphabetMode) {
setShiftLocked(state.mIsAlphabetShiftLocked);
if (!state.mIsAlphabetShiftLocked) {
- setShifted(state.mIsShifted ? MANUAL_SHIFT : UNSHIFT);
+ setShifted(state.mShiftMode);
}
} else {
mPrevMainKeyboardWasShiftLocked = state.mIsAlphabetShiftLocked;
}
}
- private static final int UNSHIFT = 0;
- private static final int MANUAL_SHIFT = 1;
- private static final int AUTOMATIC_SHIFT = 2;
- private static final int SHIFT_LOCK_SHIFTED = 3;
-
private void setShifted(final int shiftMode) {
if (DEBUG_ACTION) {
Log.d(TAG, "setShifted: shiftMode=" + shiftModeToString(shiftMode) + " " + this);
@@ -525,6 +526,9 @@ public final class KeyboardState {
} else if (mAlphabetShiftState.isShiftLockShifted() && withSliding) {
// In shift locked state, shift has been pressed and slid out to other key.
setShiftLocked(true);
+ } else if (mAlphabetShiftState.isManualShifted() && withSliding) {
+ // Shift has been pressed and slid out to other key.
+ mSwitchState = SWITCH_STATE_MOMENTARY_ALPHA_SHIFT;
} else if (isShiftLocked && !mAlphabetShiftState.isShiftLockShifted()
&& (mShiftKeyState.isPressing() || mShiftKeyState.isPressingOnShifted())
&& !withSliding) {
@@ -554,17 +558,21 @@ public final class KeyboardState {
mShiftKeyState.onRelease();
}
- public void onCancelInput(final boolean isSinglePointer) {
+ public void onFinishSlidingInput() {
if (DEBUG_EVENT) {
- Log.d(TAG, "onCancelInput: single=" + isSinglePointer + " " + this);
+ Log.d(TAG, "onFinishSlidingInput: " + this);
}
// Switch back to the previous keyboard mode if the user cancels sliding input.
- if (isSinglePointer) {
- if (mSwitchState == SWITCH_STATE_MOMENTARY_ALPHA_AND_SYMBOL) {
- toggleAlphabetAndSymbols();
- } else if (mSwitchState == SWITCH_STATE_MOMENTARY_SYMBOL_AND_MORE) {
- toggleShiftInSymbols();
- }
+ switch (mSwitchState) {
+ case SWITCH_STATE_MOMENTARY_ALPHA_AND_SYMBOL:
+ toggleAlphabetAndSymbols();
+ break;
+ case SWITCH_STATE_MOMENTARY_SYMBOL_AND_MORE:
+ toggleShiftInSymbols();
+ break;
+ case SWITCH_STATE_MOMENTARY_ALPHA_SHIFT:
+ setAlphabetKeyboard();
+ break;
}
}
@@ -577,10 +585,9 @@ public final class KeyboardState {
return c == Constants.CODE_SPACE || c == Constants.CODE_ENTER;
}
- public void onCodeInput(final int code, final boolean isSinglePointer, final int autoCaps) {
+ public void onCodeInput(final int code, final int autoCaps) {
if (DEBUG_EVENT) {
Log.d(TAG, "onCodeInput: code=" + Constants.printableCode(code)
- + " single=" + isSinglePointer
+ " autoCaps=" + autoCaps + " " + this);
}
@@ -593,23 +600,12 @@ public final class KeyboardState {
} else {
mSwitchState = SWITCH_STATE_SYMBOL_BEGIN;
}
- } else if (isSinglePointer) {
- // Switch back to the previous keyboard mode if the user pressed the mode change key
- // and slid to other key, then released the finger.
- // If the user cancels the sliding input, switching back to the previous keyboard
- // mode is handled by {@link #onCancelInput}.
- toggleAlphabetAndSymbols();
}
break;
case SWITCH_STATE_MOMENTARY_SYMBOL_AND_MORE:
if (code == Constants.CODE_SHIFT) {
// Detected only the shift key has been pressed on symbol layout, and then released.
mSwitchState = SWITCH_STATE_SYMBOL_BEGIN;
- } else if (isSinglePointer) {
- // Switch back to the previous keyboard mode if the user pressed the shift key on
- // symbol mode and slid to other key, then released the finger.
- toggleShiftInSymbols();
- mSwitchState = SWITCH_STATE_SYMBOL;
}
break;
case SWITCH_STATE_SYMBOL_BEGIN:
@@ -634,7 +630,7 @@ public final class KeyboardState {
}
}
- private static String shiftModeToString(final int shiftMode) {
+ static String shiftModeToString(final int shiftMode) {
switch (shiftMode) {
case UNSHIFT: return "UNSHIFT";
case MANUAL_SHIFT: return "MANUAL";
@@ -650,6 +646,7 @@ public final class KeyboardState {
case SWITCH_STATE_SYMBOL: return "SYMBOL";
case SWITCH_STATE_MOMENTARY_ALPHA_AND_SYMBOL: return "MOMENTARY-ALPHA-SYMBOL";
case SWITCH_STATE_MOMENTARY_SYMBOL_AND_MORE: return "MOMENTARY-SYMBOL-MORE";
+ case SWITCH_STATE_MOMENTARY_ALPHA_SHIFT: return "MOMENTARY-ALPHA_SHIFT";
default: return null;
}
}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueue.java b/java/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueue.java
index 6bc6acc0f..31ef3cd8f 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueue.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueue.java
@@ -30,7 +30,7 @@ public final class PointerTrackerQueue {
public boolean isModifier();
public boolean isInSlidingKeyInput();
public void onPhantomUpEvent(long eventTime);
- public void cancelTracking();
+ public void cancelTrackingForAction();
}
private static final int INITIAL_CAPACITY = 10;
@@ -48,6 +48,9 @@ public final class PointerTrackerQueue {
public void add(final Element pointer) {
synchronized (mExpandableArrayOfActivePointers) {
+ if (DEBUG) {
+ Log.d(TAG, "add: " + pointer + " " + this);
+ }
final ArrayList<Element> expandableArray = mExpandableArrayOfActivePointers;
final int arraySize = mArraySize;
if (arraySize < expandableArray.size()) {
@@ -61,24 +64,27 @@ public final class PointerTrackerQueue {
public void remove(final Element pointer) {
synchronized (mExpandableArrayOfActivePointers) {
+ if (DEBUG) {
+ Log.d(TAG, "remove: " + pointer + " " + this);
+ }
final ArrayList<Element> expandableArray = mExpandableArrayOfActivePointers;
final int arraySize = mArraySize;
- int newSize = 0;
+ int newIndex = 0;
for (int index = 0; index < arraySize; index++) {
final Element element = expandableArray.get(index);
if (element == pointer) {
- if (newSize != index) {
+ if (newIndex != index) {
Log.w(TAG, "Found duplicated element in remove: " + pointer);
}
continue; // Remove this element from the expandableArray.
}
- if (newSize != index) {
+ if (newIndex != index) {
// Shift this element toward the beginning of the expandableArray.
- expandableArray.set(newSize, element);
+ expandableArray.set(newIndex, element);
}
- newSize++;
+ newIndex++;
}
- mArraySize = newSize;
+ mArraySize = newIndex;
}
}
@@ -95,8 +101,8 @@ public final class PointerTrackerQueue {
}
final ArrayList<Element> expandableArray = mExpandableArrayOfActivePointers;
final int arraySize = mArraySize;
- int newSize, index;
- for (newSize = index = 0; index < arraySize; index++) {
+ int newIndex, index;
+ for (newIndex = index = 0; index < arraySize; index++) {
final Element element = expandableArray.get(index);
if (element == pointer) {
break; // Stop releasing elements.
@@ -105,29 +111,30 @@ public final class PointerTrackerQueue {
element.onPhantomUpEvent(eventTime);
continue; // Remove this element from the expandableArray.
}
- if (newSize != index) {
+ if (newIndex != index) {
// Shift this element toward the beginning of the expandableArray.
- expandableArray.set(newSize, element);
+ expandableArray.set(newIndex, element);
}
- newSize++;
+ newIndex++;
}
// Shift rest of the expandableArray.
int count = 0;
for (; index < arraySize; index++) {
final Element element = expandableArray.get(index);
if (element == pointer) {
- if (count > 0) {
+ count++;
+ if (count > 1) {
Log.w(TAG, "Found duplicated element in releaseAllPointersOlderThan: "
+ pointer);
}
- count++;
}
- if (newSize != index) {
- expandableArray.set(newSize, expandableArray.get(index));
- newSize++;
+ if (newIndex != index) {
+ // Shift this element toward the beginning of the expandableArray.
+ expandableArray.set(newIndex, expandableArray.get(index));
}
+ newIndex++;
}
- mArraySize = newSize;
+ mArraySize = newIndex;
}
}
@@ -146,26 +153,26 @@ public final class PointerTrackerQueue {
}
final ArrayList<Element> expandableArray = mExpandableArrayOfActivePointers;
final int arraySize = mArraySize;
- int newSize = 0, count = 0;
+ int newIndex = 0, count = 0;
for (int index = 0; index < arraySize; index++) {
final Element element = expandableArray.get(index);
if (element == pointer) {
- if (count > 0) {
+ count++;
+ if (count > 1) {
Log.w(TAG, "Found duplicated element in releaseAllPointersExcept: "
+ pointer);
}
- count++;
} else {
element.onPhantomUpEvent(eventTime);
continue; // Remove this element from the expandableArray.
}
- if (newSize != index) {
+ if (newIndex != index) {
// Shift this element toward the beginning of the expandableArray.
- expandableArray.set(newSize, element);
+ expandableArray.set(newIndex, element);
}
- newSize++;
+ newIndex++;
}
- mArraySize = newSize;
+ mArraySize = newIndex;
}
}
@@ -202,11 +209,14 @@ public final class PointerTrackerQueue {
public void cancelAllPointerTracker() {
synchronized (mExpandableArrayOfActivePointers) {
+ if (DEBUG) {
+ Log.d(TAG, "cancelAllPointerTracker: " + this);
+ }
final ArrayList<Element> expandableArray = mExpandableArrayOfActivePointers;
final int arraySize = mArraySize;
for (int index = 0; index < arraySize; index++) {
final Element element = expandableArray.get(index);
- element.cancelTracking();
+ element.cancelTrackingForAction();
}
}
}
diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionary.java b/java/src/com/android/inputmethod/latin/BinaryDictionary.java
index dbc2b9082..4fc1919dc 100644
--- a/java/src/com/android/inputmethod/latin/BinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/BinaryDictionary.java
@@ -107,13 +107,16 @@ public final class BinaryDictionary extends Dictionary {
@Override
public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer,
- final String prevWord, final ProximityInfo proximityInfo) {
- return getSuggestionsWithSessionId(composer, prevWord, proximityInfo, 0);
+ final String prevWord, final ProximityInfo proximityInfo,
+ final boolean blockOffensiveWords) {
+ return getSuggestionsWithSessionId(composer, prevWord, proximityInfo, blockOffensiveWords,
+ 0 /* sessionId */);
}
@Override
public ArrayList<SuggestedWordInfo> getSuggestionsWithSessionId(final WordComposer composer,
- final String prevWord, final ProximityInfo proximityInfo, int sessionId) {
+ final String prevWord, final ProximityInfo proximityInfo,
+ final boolean blockOffensiveWords, final int sessionId) {
if (!isValidDictionary()) return null;
Arrays.fill(mInputCodePoints, Constants.NOT_A_CODE);
@@ -147,10 +150,21 @@ public final class BinaryDictionary extends Dictionary {
++len;
}
if (len > 0) {
- final int score = SuggestedWordInfo.KIND_WHITELIST == mOutputTypes[j]
+ final int flags = mOutputTypes[j] & SuggestedWordInfo.KIND_MASK_FLAGS;
+ if (blockOffensiveWords
+ && 0 != (flags & SuggestedWordInfo.KIND_FLAG_POSSIBLY_OFFENSIVE)
+ && 0 == (flags & SuggestedWordInfo.KIND_FLAG_EXACT_MATCH)) {
+ // If we block potentially offensive words, and if the word is possibly
+ // offensive, then we don't output it unless it's also an exact match.
+ continue;
+ }
+ final int kind = mOutputTypes[j] & SuggestedWordInfo.KIND_MASK_KIND;
+ final int score = SuggestedWordInfo.KIND_WHITELIST == kind
? SuggestedWordInfo.MAX_SCORE : mOutputScores[j];
+ // TODO: check that all users of the `kind' parameter are ready to accept
+ // flags too and pass mOutputTypes[j] instead of kind
suggestions.add(new SuggestedWordInfo(new String(mOutputCodePoints, start, len),
- score, mOutputTypes[j], mDictType));
+ score, kind, mDictType));
}
}
return suggestions;
diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java b/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java
index 42f713697..a9b58de44 100644
--- a/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java
+++ b/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java
@@ -81,6 +81,7 @@ public final class BinaryDictionaryFileDumper {
private static final String QUERY_PATH_METADATA = "metadata";
private static final String INSERT_METADATA_CLIENT_ID_COLUMN = "clientid";
private static final String INSERT_METADATA_METADATA_URI_COLUMN = "uri";
+ private static final String INSERT_METADATA_METADATA_ADDITIONAL_ID_COLUMN = "additionalid";
// Prevents this class to be accidentally instantiated.
private BinaryDictionaryFileDumper() {
@@ -209,7 +210,7 @@ public final class BinaryDictionaryFileDumper {
* to the cache file name designated by its id and locale, overwriting it if already present
* and creating it (and its containing directory) if necessary.
*/
- private static AssetFileAddress cacheWordList(final String wordlistId, final String locale,
+ private static void cacheWordList(final String wordlistId, final String locale,
final ContentProviderClient providerClient, final Context context) {
final int COMPRESSED_CRYPTED_COMPRESSED = 0;
final int CRYPTED_COMPRESSED = 1;
@@ -227,7 +228,7 @@ public final class BinaryDictionaryFileDumper {
providerClient, QUERY_PATH_DATAFILE, wordlistId /* extraPath */);
} catch (RemoteException e) {
Log.e(TAG, "Can't communicate with the dictionary pack", e);
- return null;
+ return;
}
final String finalFileName =
DictionaryInfoUtils.getCacheFileName(wordlistId, locale, context);
@@ -236,11 +237,11 @@ public final class BinaryDictionaryFileDumper {
tempFileName = BinaryDictionaryGetter.getTempFileName(wordlistId, context);
} catch (IOException e) {
Log.e(TAG, "Can't open the temporary file", e);
- return null;
+ return;
}
for (int mode = MODE_MIN; mode <= MODE_MAX; ++mode) {
- InputStream originalSourceStream = null;
+ final InputStream originalSourceStream;
InputStream inputStream = null;
InputStream uncompressedStream = null;
InputStream decryptedStream = null;
@@ -253,7 +254,7 @@ public final class BinaryDictionaryFileDumper {
// Open input.
afd = openAssetFileDescriptor(providerClient, wordListUri);
// If we can't open it at all, don't even try a number of times.
- if (null == afd) return null;
+ if (null == afd) return;
originalSourceStream = afd.createInputStream();
// Open output.
outputFile = new File(tempFileName);
@@ -304,7 +305,7 @@ public final class BinaryDictionaryFileDumper {
}
BinaryDictionaryGetter.removeFilesWithIdExcept(context, wordlistId, finalFile);
// Success! Close files (through the finally{} clause) and return.
- return AssetFileAddress.makeFromFileName(finalFileName);
+ return;
} catch (Exception e) {
if (DEBUG) {
Log.i(TAG, "Can't open word list in mode " + mode, e);
@@ -319,7 +320,7 @@ public final class BinaryDictionaryFileDumper {
} finally {
// Ignore exceptions while closing files.
try {
- // inputStream.close() will close afd, we should not call afd.close().
+ if (null != afd) afd.close();
if (null != inputStream) inputStream.close();
if (null != uncompressedStream) uncompressedStream.close();
if (null != decryptedStream) decryptedStream.close();
@@ -349,7 +350,6 @@ public final class BinaryDictionaryFileDumper {
} catch (RemoteException e) {
Log.e(TAG, "In addition, communication with the dictionary provider was cut", e);
}
- return null;
}
/**
@@ -358,30 +358,23 @@ public final class BinaryDictionaryFileDumper {
* This will query a content provider for word list data for a given locale, and copy the
* files locally so that they can be mmap'ed. This may overwrite previously cached word lists
* with newer versions if a newer version is made available by the content provider.
- * @returns the addresses of the word list files, or null if no data could be obtained.
* @throw FileNotFoundException if the provider returns non-existent data.
* @throw IOException if the provider-returned data could not be read.
*/
- public static List<AssetFileAddress> cacheWordListsFromContentProvider(final Locale locale,
+ public static void cacheWordListsFromContentProvider(final Locale locale,
final Context context, final boolean hasDefaultWordList) {
final ContentProviderClient providerClient = context.getContentResolver().
acquireContentProviderClient(getProviderUriBuilder("").build());
if (null == providerClient) {
Log.e(TAG, "Can't establish communication with the dictionary provider");
- return CollectionUtils.newArrayList();
+ return;
}
try {
final List<WordListInfo> idList = getWordListWordListInfos(locale, context,
hasDefaultWordList);
- final ArrayList<AssetFileAddress> fileAddressList = CollectionUtils.newArrayList();
for (WordListInfo id : idList) {
- final AssetFileAddress afd =
- cacheWordList(id.mId, id.mLocale, providerClient, context);
- if (null != afd) {
- fileAddressList.add(afd);
- }
+ cacheWordList(id.mId, id.mLocale, providerClient, context);
}
- return fileAddressList;
} finally {
providerClient.release();
}
@@ -423,6 +416,7 @@ public final class BinaryDictionaryFileDumper {
private static void reinitializeClientRecordInDictionaryContentProvider(final Context context,
final ContentProviderClient client, final String clientId) throws RemoteException {
final String metadataFileUri = MetadataFileUriGetter.getMetadataUri(context);
+ final String metadataAdditionalId = MetadataFileUriGetter.getMetadataAdditionalId(context);
if (TextUtils.isEmpty(metadataFileUri)) return;
// Tell the content provider to reset all information about this client id
final Uri metadataContentUri = getProviderUriBuilder(clientId)
@@ -434,6 +428,7 @@ public final class BinaryDictionaryFileDumper {
final ContentValues metadataValues = new ContentValues();
metadataValues.put(INSERT_METADATA_CLIENT_ID_COLUMN, clientId);
metadataValues.put(INSERT_METADATA_METADATA_URI_COLUMN, metadataFileUri);
+ metadataValues.put(INSERT_METADATA_METADATA_ADDITIONAL_ID_COLUMN, metadataAdditionalId);
client.insert(metadataContentUri, metadataValues);
// Update the dictionary list.
diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java b/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java
index 294312843..98eadcacb 100644
--- a/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java
+++ b/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java
@@ -72,10 +72,16 @@ final class BinaryDictionaryGetter {
public static String getTempFileName(final String id, final Context context)
throws IOException {
final String safeId = DictionaryInfoUtils.replaceFileNameDangerousCharacters(id);
+ final File directory = new File(DictionaryInfoUtils.getWordListTempDirectory(context));
+ if (!directory.exists()) {
+ if (!directory.mkdirs()) {
+ Log.e(TAG, "Could not create the temporary directory");
+ }
+ }
// If the first argument is less than three chars, createTempFile throws a
// RuntimeException. We don't really care about what name we get, so just
// put a three-chars prefix makes us safe.
- return File.createTempFile("xxx" + safeId, null).getAbsolutePath();
+ return File.createTempFile("xxx" + safeId, null, directory).getAbsolutePath();
}
/**
@@ -89,8 +95,16 @@ final class BinaryDictionaryGetter {
+ fallbackResId);
return null;
}
- return AssetFileAddress.makeFromFileNameAndOffset(
- context.getApplicationInfo().sourceDir, afd.getStartOffset(), afd.getLength());
+ try {
+ return AssetFileAddress.makeFromFileNameAndOffset(
+ context.getApplicationInfo().sourceDir, afd.getStartOffset(), afd.getLength());
+ } finally {
+ try {
+ afd.close();
+ } catch (IOException e) {
+ // Ignored
+ }
+ }
}
private static final class DictPackSettings {
@@ -276,9 +290,6 @@ final class BinaryDictionaryGetter {
final Context context) {
final boolean hasDefaultWordList = DictionaryFactory.isDictionaryAvailable(context, locale);
- // cacheWordListsFromContentProvider returns the list of files it copied to local
- // storage, but we don't really care about what was copied NOW: what we want is the
- // list of everything we ever cached, so we ignore the return value.
// TODO: The development-only-diagnostic version is not supported by the Dictionary Pack
// Service yet
if (!ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
diff --git a/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java b/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java
index 75c2cf2c8..b9db9a092 100644
--- a/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java
@@ -16,21 +16,26 @@
package com.android.inputmethod.latin;
+import com.android.inputmethod.latin.personalization.AccountUtils;
+
import android.content.ContentResolver;
import android.content.Context;
import android.database.ContentObserver;
import android.database.Cursor;
+import android.net.Uri;
import android.os.SystemClock;
import android.provider.BaseColumns;
+import android.provider.ContactsContract;
import android.provider.ContactsContract.Contacts;
import android.text.TextUtils;
import android.util.Log;
+import java.util.List;
import java.util.Locale;
public class ContactsBinaryDictionary extends ExpandableBinaryDictionary {
- private static final String[] PROJECTION = {BaseColumns._ID, Contacts.DISPLAY_NAME,};
+ private static final String[] PROJECTION = {BaseColumns._ID, Contacts.DISPLAY_NAME};
private static final String[] PROJECTION_ID_ONLY = {BaseColumns._ID};
private static final String TAG = ContactsBinaryDictionary.class.getSimpleName();
@@ -102,9 +107,32 @@ public class ContactsBinaryDictionary extends ExpandableBinaryDictionary {
@Override
public void loadDictionaryAsync() {
+ clearFusionDictionary();
+ loadDeviceAccountsEmailAddresses();
+ loadDictionaryAsyncForUri(ContactsContract.Profile.CONTENT_URI);
+ // TODO: Switch this URL to the newer ContactsContract too
+ loadDictionaryAsyncForUri(Contacts.CONTENT_URI);
+ }
+
+ private void loadDeviceAccountsEmailAddresses() {
+ final List<String> accountVocabulary =
+ AccountUtils.getDeviceAccountsEmailAddresses(mContext);
+ if (accountVocabulary == null || accountVocabulary.isEmpty()) {
+ return;
+ }
+ for (String word : accountVocabulary) {
+ if (DEBUG) {
+ Log.d(TAG, "loadAccountVocabulary: " + word);
+ }
+ super.addWord(word, null /* shortcut */, FREQUENCY_FOR_CONTACTS,
+ false /* isNotAWord */);
+ }
+ }
+
+ private void loadDictionaryAsyncForUri(final Uri uri) {
try {
Cursor cursor = mContext.getContentResolver()
- .query(Contacts.CONTENT_URI, PROJECTION, null, null, null);
+ .query(uri, PROJECTION, null, null, null);
if (cursor != null) {
try {
if (cursor.moveToFirst()) {
@@ -129,7 +157,6 @@ public class ContactsBinaryDictionary extends ExpandableBinaryDictionary {
}
private void addWords(final Cursor cursor) {
- clearFusionDictionary();
int count = 0;
while (!cursor.isAfterLast() && count < MAX_CONTACT_COUNT) {
String name = cursor.getString(INDEX_NAME);
@@ -173,6 +200,9 @@ public class ContactsBinaryDictionary extends ExpandableBinaryDictionary {
// capitalization of i.
final int wordLen = StringUtils.codePointCount(word);
if (wordLen < MAX_WORD_LENGTH && wordLen > 1) {
+ if (DEBUG) {
+ Log.d(TAG, "addName " + name + ", " + word + ", " + prevWord);
+ }
super.addWord(word, null /* shortcut */, FREQUENCY_FOR_CONTACTS,
false /* isNotAWord */);
if (!TextUtils.isEmpty(prevWord)) {
diff --git a/java/src/com/android/inputmethod/latin/DebugSettings.java b/java/src/com/android/inputmethod/latin/DebugSettings.java
index c2aade64d..5969a63de 100644
--- a/java/src/com/android/inputmethod/latin/DebugSettings.java
+++ b/java/src/com/android/inputmethod/latin/DebugSettings.java
@@ -121,18 +121,8 @@ public final class DebugSettings extends PreferenceFragment
return;
}
boolean isDebugMode = mDebugMode.isChecked();
- String version = "";
- try {
- final Context context = getActivity();
- if (context == null) {
- return;
- }
- final String packageName = context.getPackageName();
- PackageInfo info = context.getPackageManager().getPackageInfo(packageName, 0);
- version = "Version " + info.versionName;
- } catch (NameNotFoundException e) {
- Log.e(TAG, "Could not find version info.");
- }
+ final String version = getResources().getString(
+ R.string.version_text, Utils.getVersionName(getActivity()));
if (!isDebugMode) {
mDebugMode.setTitle(version);
mDebugMode.setSummary("");
diff --git a/java/src/com/android/inputmethod/latin/Dictionary.java b/java/src/com/android/inputmethod/latin/Dictionary.java
index 9691fa231..acd7c2aa1 100644
--- a/java/src/com/android/inputmethod/latin/Dictionary.java
+++ b/java/src/com/android/inputmethod/latin/Dictionary.java
@@ -51,18 +51,21 @@ public abstract class Dictionary {
* @param composer the key sequence to match with coordinate info, as a WordComposer
* @param prevWord the previous word, or null if none
* @param proximityInfo the object for key proximity. May be ignored by some implementations.
+ * @param blockOffensiveWords whether to block potentially offensive words
* @return the list of suggestions (possibly null if none)
*/
// TODO: pass more context than just the previous word, to enable better suggestions (n-gram
// and more)
abstract public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer,
- final String prevWord, final ProximityInfo proximityInfo);
+ final String prevWord, final ProximityInfo proximityInfo,
+ final boolean blockOffensiveWords);
// The default implementation of this method ignores sessionId.
// Subclasses that want to use sessionId need to override this method.
public ArrayList<SuggestedWordInfo> getSuggestionsWithSessionId(final WordComposer composer,
- final String prevWord, final ProximityInfo proximityInfo, final int sessionId) {
- return getSuggestions(composer, prevWord, proximityInfo);
+ final String prevWord, final ProximityInfo proximityInfo,
+ final boolean blockOffensiveWords, final int sessionId) {
+ return getSuggestions(composer, prevWord, proximityInfo, blockOffensiveWords);
}
/**
diff --git a/java/src/com/android/inputmethod/latin/DictionaryCollection.java b/java/src/com/android/inputmethod/latin/DictionaryCollection.java
index 2832ad43c..ed2b44223 100644
--- a/java/src/com/android/inputmethod/latin/DictionaryCollection.java
+++ b/java/src/com/android/inputmethod/latin/DictionaryCollection.java
@@ -56,18 +56,19 @@ public final class DictionaryCollection extends Dictionary {
@Override
public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer,
- final String prevWord, final ProximityInfo proximityInfo) {
+ final String prevWord, final ProximityInfo proximityInfo,
+ final boolean blockOffensiveWords) {
final CopyOnWriteArrayList<Dictionary> dictionaries = mDictionaries;
if (dictionaries.isEmpty()) return null;
// To avoid creating unnecessary objects, we get the list out of the first
// dictionary and add the rest to it if not null, hence the get(0)
ArrayList<SuggestedWordInfo> suggestions = dictionaries.get(0).getSuggestions(composer,
- prevWord, proximityInfo);
+ prevWord, proximityInfo, blockOffensiveWords);
if (null == suggestions) suggestions = CollectionUtils.newArrayList();
final int length = dictionaries.size();
for (int i = 1; i < length; ++ i) {
final ArrayList<SuggestedWordInfo> sugg = dictionaries.get(i).getSuggestions(composer,
- prevWord, proximityInfo);
+ prevWord, proximityInfo, blockOffensiveWords);
if (null != sugg) suggestions.addAll(sugg);
}
return suggestions;
diff --git a/java/src/com/android/inputmethod/latin/DictionaryInfoUtils.java b/java/src/com/android/inputmethod/latin/DictionaryInfoUtils.java
index dcfa483f8..df7bad8d0 100644
--- a/java/src/com/android/inputmethod/latin/DictionaryInfoUtils.java
+++ b/java/src/com/android/inputmethod/latin/DictionaryInfoUtils.java
@@ -129,6 +129,13 @@ public class DictionaryInfoUtils {
}
/**
+ * Helper method to get the top level temp directory.
+ */
+ public static String getWordListTempDirectory(final Context context) {
+ return context.getFilesDir() + File.separator + "tmp";
+ }
+
+ /**
* Reverse escaping done by replaceFileNameDangerousCharacters.
*/
public static String getWordListIdFromFileName(final String fname) {
diff --git a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
index 4b1975a00..887d657e9 100644
--- a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
@@ -200,12 +200,14 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
@Override
public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer,
- final String prevWord, final ProximityInfo proximityInfo) {
+ final String prevWord, final ProximityInfo proximityInfo,
+ final boolean blockOffensiveWords) {
asyncReloadDictionaryIfRequired();
if (mLocalDictionaryController.tryLock()) {
try {
if (mBinaryDictionary != null) {
- return mBinaryDictionary.getSuggestions(composer, prevWord, proximityInfo);
+ return mBinaryDictionary.getSuggestions(composer, prevWord, proximityInfo,
+ blockOffensiveWords);
}
} finally {
mLocalDictionaryController.unlock();
diff --git a/java/src/com/android/inputmethod/latin/ExpandableDictionary.java b/java/src/com/android/inputmethod/latin/ExpandableDictionary.java
index fd81d13ca..0dabdb835 100644
--- a/java/src/com/android/inputmethod/latin/ExpandableDictionary.java
+++ b/java/src/com/android/inputmethod/latin/ExpandableDictionary.java
@@ -253,7 +253,8 @@ public class ExpandableDictionary extends Dictionary {
@Override
public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer,
- final String prevWord, final ProximityInfo proximityInfo) {
+ final String prevWord, final ProximityInfo proximityInfo,
+ final boolean blockOffensiveWords) {
if (reloadDictionaryIfRequired()) return null;
if (composer.size() > 1) {
if (composer.size() >= Constants.Dictionary.MAX_WORD_LENGTH) {
diff --git a/java/src/com/android/inputmethod/latin/FeedbackUtils.java b/java/src/com/android/inputmethod/latin/FeedbackUtils.java
index 1e5260e34..0582763fe 100644
--- a/java/src/com/android/inputmethod/latin/FeedbackUtils.java
+++ b/java/src/com/android/inputmethod/latin/FeedbackUtils.java
@@ -17,6 +17,7 @@
package com.android.inputmethod.latin;
import android.content.Context;
+import android.content.Intent;
public class FeedbackUtils {
public static boolean isFeedbackFormSupported() {
@@ -25,4 +26,12 @@ public class FeedbackUtils {
public static void showFeedbackForm(Context context) {
}
+
+ public static int getAboutKeyboardTitleResId() {
+ return 0;
+ }
+
+ public static Intent getAboutKeyboardIntent(Context context) {
+ return null;
+ }
}
diff --git a/java/src/com/android/inputmethod/latin/JniUtils.java b/java/src/com/android/inputmethod/latin/JniUtils.java
index f9305991a..8aedee576 100644
--- a/java/src/com/android/inputmethod/latin/JniUtils.java
+++ b/java/src/com/android/inputmethod/latin/JniUtils.java
@@ -23,15 +23,19 @@ import com.android.inputmethod.latin.define.JniLibName;
public final class JniUtils {
private static final String TAG = JniUtils.class.getSimpleName();
- private JniUtils() {
- // This utility class is not publicly instantiable.
- }
-
- public static void loadNativeLibrary() {
+ static {
try {
System.loadLibrary(JniLibName.JNI_LIB_NAME);
} catch (UnsatisfiedLinkError ule) {
Log.e(TAG, "Could not load native library " + JniLibName.JNI_LIB_NAME, ule);
}
}
+
+ private JniUtils() {
+ // This utility class is not publicly instantiable.
+ }
+
+ public static void loadNativeLibrary() {
+ // Ensures the static initializer is called
+ }
}
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index 5ef2120c6..347a4c63a 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -90,7 +90,7 @@ import java.util.TreeSet;
/**
* Input method implementation for Qwerty'ish keyboard.
*/
-public final class LatinIME extends InputMethodService implements KeyboardActionListener,
+public class LatinIME extends InputMethodService implements KeyboardActionListener,
SuggestionStripView.Listener, TargetApplicationGetter.OnTargetApplicationKnownListener,
Suggest.SuggestInitializationListener {
private static final String TAG = LatinIME.class.getSimpleName();
@@ -188,6 +188,8 @@ public final class LatinIME extends InputMethodService implements KeyboardAction
// Keeps track of most recently inserted text (multi-character key) for reverting
private String mEnteredText;
+ // TODO: This boolean is persistent state and causes large side effects at unexpected times.
+ // Find a way to remove it for readability.
private boolean mIsAutoCorrectionIndicatorOn;
private AlertDialog mOptionsDialog;
@@ -250,6 +252,7 @@ public final class LatinIME extends InputMethodService implements KeyboardAction
}
public void postResumeSuggestions() {
+ removeMessages(MSG_RESUME_SUGGESTIONS);
sendMessageDelayed(obtainMessage(MSG_RESUME_SUGGESTIONS), mDelayUpdateSuggestions);
}
@@ -759,7 +762,8 @@ public final class LatinIME extends InputMethodService implements KeyboardAction
}
mSuggestedWords = SuggestedWords.EMPTY;
- mConnection.resetCachesUponCursorMove(editorInfo.initialSelStart);
+ mConnection.resetCachesUponCursorMove(editorInfo.initialSelStart,
+ false /* shouldFinishComposition */);
if (isDifferentTextField) {
mainKeyboardView.closing();
@@ -900,7 +904,12 @@ public final class LatinIME extends InputMethodService implements KeyboardAction
// we know for sure the cursor moved while we were composing and we should reset
// the state.
final boolean noComposingSpan = composingSpanStart == -1 && composingSpanEnd == -1;
- if (!mExpectingUpdateSelection
+ // If the keyboard is not visible, we don't need to do all the housekeeping work, as it
+ // will be reset when the keyboard shows up anyway.
+ // TODO: revisit this when LatinIME supports hardware keyboards.
+ // NOTE: the test harness subclasses LatinIME and overrides isInputViewShown().
+ // TODO: find a better way to simulate actual execution.
+ if (isInputViewShown() && !mExpectingUpdateSelection
&& !mConnection.isBelatedExpectedUpdate(oldSelStart, newSelStart)) {
// TAKE CARE: there is a race condition when we enter this test even when the user
// did not explicitly move the cursor. This happens when typing fast, where two keys
@@ -1148,13 +1157,14 @@ public final class LatinIME extends InputMethodService implements KeyboardAction
// This will reset the whole input state to the starting state. It will clear
// the composing word, reset the last composed word, tell the inputconnection about it.
private void resetEntireInputState(final int newCursorPosition) {
+ final boolean shouldFinishComposition = mWordComposer.isComposingWord();
resetComposingState(true /* alsoResetLastComposedWord */);
if (mSettings.getCurrent().mBigramPredictionEnabled) {
clearSuggestionStrip();
} else {
setSuggestedWords(mSettings.getCurrent().mSuggestPuncList, false);
}
- mConnection.resetCachesUponCursorMove(newCursorPosition);
+ mConnection.resetCachesUponCursorMove(newCursorPosition, shouldFinishComposition);
}
private void resetComposingState(final boolean alsoResetLastComposedWord) {
@@ -1577,21 +1587,11 @@ public final class LatinIME extends InputMethodService implements KeyboardAction
commitTyped(LastComposedWord.NOT_A_SEPARATOR);
}
mExpectingUpdateSelection = true;
- // The following is necessary for the case where the user typed something but didn't
- // manual pick it and didn't input any separator: we want to put a space between what
- // has been entered and the coming gesture input result, so we go into phantom space
- // state, which will be promoted to a space when the gesture result is committed. But if
- // the current input ends in a word connector on the other hand, then we want to have
- // the next input stick to the current input so we don't switch to phantom space state.
- if (!mSettings.getCurrent().isWordConnector(lastChar)) {
- mSpaceState = SPACE_STATE_PHANTOM;
- }
- } else {
- final int codePointBeforeCursor = mConnection.getCodePointBeforeCursor();
- if (Character.isLetter(codePointBeforeCursor)
- || mSettings.getCurrent().isUsuallyFollowedBySpace(codePointBeforeCursor)) {
- mSpaceState = SPACE_STATE_PHANTOM;
- }
+ }
+ final int codePointBeforeCursor = mConnection.getCodePointBeforeCursor();
+ if (Character.isLetterOrDigit(codePointBeforeCursor)
+ || mSettings.getCurrent().isUsuallyFollowedBySpace(codePointBeforeCursor)) {
+ mSpaceState = SPACE_STATE_PHANTOM;
}
mConnection.endBatchEdit();
mWordComposer.setCapitalizedModeAtStartComposingTime(getActualCapsMode());
@@ -1759,9 +1759,16 @@ public final class LatinIME extends InputMethodService implements KeyboardAction
// Called from PointerTracker through the KeyboardActionListener interface
@Override
+ public void onFinishSlidingInput() {
+ // User finished sliding input.
+ mKeyboardSwitcher.onFinishSlidingInput();
+ }
+
+ // Called from PointerTracker through the KeyboardActionListener interface
+ @Override
public void onCancelInput() {
// User released a finger outside any key
- mKeyboardSwitcher.onCancelInput();
+ // Nothing to do so far.
}
@Override
@@ -1905,6 +1912,8 @@ public final class LatinIME extends InputMethodService implements KeyboardAction
final int y, final int spaceState) {
boolean isComposingWord = mWordComposer.isComposingWord();
+ // TODO: remove isWordConnector() and use isUsuallyFollowedBySpace() instead.
+ // See onStartBatchInput() to see how to do it.
if (SPACE_STATE_PHANTOM == spaceState &&
!mSettings.getCurrent().isWordConnector(primaryCode)) {
if (isComposingWord) {
@@ -2185,6 +2194,7 @@ public final class LatinIME extends InputMethodService implements KeyboardAction
mConnection.getNthPreviousWord(mSettings.getCurrent().mWordSeparators,
mWordComposer.isComposingWord() ? 2 : 1);
return mSuggest.getSuggestedWords(mWordComposer, prevWord, keyboard.getProximityInfo(),
+ mSettings.getBlockPotentiallyOffensive(),
mSettings.getCurrent().mCorrectionEnabled, sessionId);
}
@@ -2443,10 +2453,15 @@ public final class LatinIME extends InputMethodService implements KeyboardAction
private void restartSuggestionsOnWordTouchedByCursor() {
// If the cursor is not touching a word, or if there is a selection, return right away.
if (mLastSelectionStart != mLastSelectionEnd) return;
+ // If we don't know the cursor location, return.
+ if (mLastSelectionStart < 0) return;
if (!mConnection.isCursorTouchingWord(mSettings.getCurrent())) return;
final Range range = mConnection.getWordRangeAtCursor(mSettings.getWordSeparators(),
0 /* additionalPrecedingWordsCount */);
if (null == range) return; // Happens if we don't have an input connection at all
+ // If for some strange reason (editor bug or so) we measure the text before the cursor as
+ // longer than what the entire text is supposed to be, the safe thing to do is bail out.
+ if (range.mCharsBefore > mLastSelectionStart) return;
final ArrayList<SuggestedWordInfo> suggestions = CollectionUtils.newArrayList();
final String typedWord = range.mWord.toString();
if (range.mWord instanceof SpannableString) {
@@ -2499,7 +2514,10 @@ public final class LatinIME extends InputMethodService implements KeyboardAction
// Note that it's very important here that suggestedWords.mWillAutoCorrect is false.
// We never want to auto-correct on a resumed suggestion. Please refer to the three
- // places above where suggestedWords is affected.
+ // places above where suggestedWords is affected. We also need to reset
+ // mIsAutoCorrectionIndicatorOn to avoid showSuggestionStrip touching the text to adapt it.
+ // TODO: remove mIsAutoCorrectionIndicator on (see comment on definition)
+ mIsAutoCorrectionIndicatorOn = false;
showSuggestionStrip(suggestedWords, typedWord);
}
@@ -2611,8 +2629,8 @@ public final class LatinIME extends InputMethodService implements KeyboardAction
// Callback called by PointerTracker through the KeyboardActionListener. This is called when a
// key is depressed; release matching call is onReleaseKey below.
@Override
- public void onPressKey(final int primaryCode) {
- mKeyboardSwitcher.onPressKey(primaryCode);
+ public void onPressKey(final int primaryCode, final boolean isSinglePointer) {
+ mKeyboardSwitcher.onPressKey(primaryCode, isSinglePointer);
}
// Callback by PointerTracker through the KeyboardActionListener. This is called when a key
diff --git a/java/src/com/android/inputmethod/latin/MetadataFileUriGetter.java b/java/src/com/android/inputmethod/latin/MetadataFileUriGetter.java
index e6dc6db8f..a98ecc7b6 100644
--- a/java/src/com/android/inputmethod/latin/MetadataFileUriGetter.java
+++ b/java/src/com/android/inputmethod/latin/MetadataFileUriGetter.java
@@ -19,10 +19,18 @@ package com.android.inputmethod.latin;
import android.content.Context;
/**
- * Helper class to get the metadata URI.
+ * Helper class to get the metadata URI and the additional ID.
*/
public class MetadataFileUriGetter {
- public static String getMetadataUri(Context context) {
+ private MetadataFileUriGetter() {
+ // This helper class is not instantiable.
+ }
+
+ public static String getMetadataUri(final Context context) {
return context.getString(R.string.dictionary_pack_metadata_uri);
}
+
+ public static String getMetadataAdditionalId(final Context context) {
+ return "";
+ }
}
diff --git a/java/src/com/android/inputmethod/latin/ResourceUtils.java b/java/src/com/android/inputmethod/latin/ResourceUtils.java
index b74b979b5..a9fba5348 100644
--- a/java/src/com/android/inputmethod/latin/ResourceUtils.java
+++ b/java/src/com/android/inputmethod/latin/ResourceUtils.java
@@ -19,14 +19,17 @@ package com.android.inputmethod.latin;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.os.Build;
+import android.text.TextUtils;
import android.util.Log;
import android.util.TypedValue;
+import com.android.inputmethod.annotations.UsedForTesting;
+
+import java.util.ArrayList;
import java.util.HashMap;
public final class ResourceUtils {
private static final String TAG = ResourceUtils.class.getSimpleName();
- private static final boolean DEBUG = false;
public static final float UNDEFINED_RATIO = -1.0f;
public static final int UNDEFINED_DIMENSION = -1;
@@ -35,11 +38,32 @@ public final class ResourceUtils {
// This utility class is not publicly instantiable.
}
- private static final String DEFAULT_PREFIX = "DEFAULT,";
- private static final String HARDWARE_PREFIX = Build.HARDWARE + ",";
private static final HashMap<String, String> sDeviceOverrideValueMap =
CollectionUtils.newHashMap();
+ private static final String[] BUILD_KEYS_AND_VALUES = {
+ "HARDWARE", Build.HARDWARE,
+ "MODEL", Build.MODEL,
+ "BRAND", Build.BRAND,
+ "MANUFACTURER", Build.MANUFACTURER
+ };
+ private static final HashMap<String, String> sBuildKeyValues;
+ private static final String sBuildKeyValuesDebugString;
+
+ static {
+ sBuildKeyValues = CollectionUtils.newHashMap();
+ final ArrayList<String> keyValuePairs = CollectionUtils.newArrayList();
+ final int keyCount = BUILD_KEYS_AND_VALUES.length / 2;
+ for (int i = 0; i < keyCount; i++) {
+ final int index = i * 2;
+ final String key = BUILD_KEYS_AND_VALUES[index];
+ final String value = BUILD_KEYS_AND_VALUES[index + 1];
+ sBuildKeyValues.put(key, value);
+ keyValuePairs.add(key + '=' + value);
+ }
+ sBuildKeyValuesDebugString = "[" + TextUtils.join(" ", keyValuePairs) + "]";
+ }
+
public static String getDeviceOverrideValue(final Resources res, final int overrideResId) {
final int orientation = res.getConfiguration().orientation;
final String key = overrideResId + "-" + orientation;
@@ -48,33 +72,114 @@ public final class ResourceUtils {
}
final String[] overrideArray = res.getStringArray(overrideResId);
- final String overrideValue = StringUtils.findPrefixedString(HARDWARE_PREFIX, overrideArray);
+ final String overrideValue = findConstantForKeyValuePairs(sBuildKeyValues, overrideArray);
// The overrideValue might be an empty string.
if (overrideValue != null) {
- if (DEBUG) {
- Log.d(TAG, "Find override value:"
- + " resource="+ res.getResourceEntryName(overrideResId)
- + " Build.HARDWARE=" + Build.HARDWARE + " override=" + overrideValue);
- }
+ Log.i(TAG, "Find override value:"
+ + " resource="+ res.getResourceEntryName(overrideResId)
+ + " build=" + sBuildKeyValuesDebugString
+ + " override=" + overrideValue);
sDeviceOverrideValueMap.put(key, overrideValue);
return overrideValue;
}
- final String defaultValue = StringUtils.findPrefixedString(DEFAULT_PREFIX, overrideArray);
+ 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.HARDWARE=" + Build.HARDWARE);
- } else if (DEBUG) {
- Log.d(TAG, "Found default value:"
- + " resource="+ res.getResourceEntryName(overrideResId)
- + " Build.HARDWARE=" + Build.HARDWARE + " default=" + defaultValue);
+ + " build=" + sBuildKeyValuesDebugString);
+ } else {
+ Log.i(TAG, "Found default value:"
+ + " resource="+ res.getResourceEntryName(overrideResId)
+ + " build=" + sBuildKeyValuesDebugString
+ + " default=" + defaultValue);
}
sDeviceOverrideValueMap.put(key, defaultValue);
return defaultValue;
}
+ /**
+ * Find the condition that fulfills specified key value pairs from an array of
+ * "condition,constant", and return the corresponding string constant. A condition is
+ * "pattern1[:pattern2...] (or an empty string for the default). A pattern is
+ * "key=regexp_value" string. The condition matches only if all patterns of the condition
+ * are true for the specified key value pairs.
+ *
+ * For example, "condition,constant" has the following format.
+ * (See {@link ResourceUtilsTests#testFindConstantForKeyValuePairsRegexp()})
+ * - HARDWARE=mako,constantForNexus4
+ * - MODEL=Nexus 4:MANUFACTURER=LGE,constantForNexus4
+ * - ,defaultConstant
+ *
+ * @param keyValuePairs attributes to be used to look for a matched condition.
+ * @param conditionConstantArray an array of "condition,constant" elements to be searched.
+ * @return the constant part of the matched "condition,constant" element. Returns null if no
+ * condition matches.
+ */
+ @UsedForTesting
+ static String findConstantForKeyValuePairs(final HashMap<String, String> keyValuePairs,
+ final String[] conditionConstantArray) {
+ if (conditionConstantArray == null || keyValuePairs == null) {
+ return null;
+ }
+ for (final String conditionConstant : conditionConstantArray) {
+ final int posComma = conditionConstant.indexOf(',');
+ if (posComma < 0) {
+ throw new RuntimeException("Array element has no comma: " + conditionConstant);
+ }
+ final String condition = conditionConstant.substring(0, posComma);
+ if (condition.isEmpty()) {
+ // Default condition. The default condition should be searched by
+ // {@link #findConstantForDefault(String[])}.
+ continue;
+ }
+ if (fulfillsCondition(keyValuePairs, condition)) {
+ return conditionConstant.substring(posComma + 1);
+ }
+ }
+ return null;
+ }
+
+ private static boolean fulfillsCondition(final HashMap<String,String> keyValuePairs,
+ final String condition) {
+ final String[] patterns = condition.split(":");
+ // Check all patterns in a condition are true
+ for (final String pattern : patterns) {
+ final int posEqual = pattern.indexOf('=');
+ if (posEqual < 0) {
+ throw new RuntimeException("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);
+ }
+ final String patternRegexpValue = pattern.substring(posEqual + 1);
+ if (!value.matches(patternRegexpValue)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @UsedForTesting
+ static String findDefaultConstant(final String[] conditionConstantArray) {
+ 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);
+ }
+ if (posComma == 0) { // condition is empty.
+ return condition.substring(posComma + 1);
+ }
+ }
+ return null;
+ }
+
public static boolean isValidFraction(final float fraction) {
return fraction >= 0.0f;
}
diff --git a/java/src/com/android/inputmethod/latin/RichInputConnection.java b/java/src/com/android/inputmethod/latin/RichInputConnection.java
index 8ed7ab264..980215de6 100644
--- a/java/src/com/android/inputmethod/latin/RichInputConnection.java
+++ b/java/src/com/android/inputmethod/latin/RichInputConnection.java
@@ -135,13 +135,14 @@ public final class RichInputConnection {
if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
}
- public void resetCachesUponCursorMove(final int newCursorPosition) {
+ public void resetCachesUponCursorMove(final int newCursorPosition,
+ final boolean shouldFinishComposition) {
mCurrentCursorPosition = newCursorPosition;
mComposingText.setLength(0);
mCommittedTextBeforeComposingText.setLength(0);
final CharSequence textBeforeCursor = getTextBeforeCursor(DEFAULT_TEXT_CACHE_SIZE, 0);
if (null != textBeforeCursor) mCommittedTextBeforeComposingText.append(textBeforeCursor);
- if (null != mIC) {
+ if (null != mIC && shouldFinishComposition) {
mIC.finishComposingText();
if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
ResearchLogger.richInputConnection_finishComposingText();
diff --git a/java/src/com/android/inputmethod/latin/RichInputMethodManager.java b/java/src/com/android/inputmethod/latin/RichInputMethodManager.java
index e39aae958..94513e635 100644
--- a/java/src/com/android/inputmethod/latin/RichInputMethodManager.java
+++ b/java/src/com/android/inputmethod/latin/RichInputMethodManager.java
@@ -22,6 +22,7 @@ import android.content.Context;
import android.content.SharedPreferences;
import android.os.IBinder;
import android.preference.PreferenceManager;
+import android.util.Log;
import android.view.inputmethod.InputMethodInfo;
import android.view.inputmethod.InputMethodManager;
import android.view.inputmethod.InputMethodSubtype;
@@ -46,6 +47,8 @@ public final class RichInputMethodManager {
private InputMethodManagerCompatWrapper mImmWrapper;
private InputMethodInfo mInputMethodInfoOfThisIme;
+ private static final int INDEX_NOT_FOUND = -1;
+
public static RichInputMethodManager getInstance() {
sInstance.checkInitialized();
return sInstance;
@@ -97,12 +100,107 @@ public final class RichInputMethodManager {
throw new RuntimeException("Input method id for " + packageName + " not found.");
}
+ public List<InputMethodSubtype> getMyEnabledInputMethodSubtypeList(
+ boolean allowsImplicitlySelectedSubtypes) {
+ return mImmWrapper.mImm.getEnabledInputMethodSubtypeList(
+ mInputMethodInfoOfThisIme, allowsImplicitlySelectedSubtypes);
+ }
+
public boolean switchToNextInputMethod(final IBinder token, final boolean onlyCurrentIme) {
- final boolean result = mImmWrapper.switchToNextInputMethod(token, onlyCurrentIme);
- if (!result) {
- mImmWrapper.mImm.switchToLastInputMethod(token);
+ if (mImmWrapper.switchToNextInputMethod(token, onlyCurrentIme)) {
+ return true;
+ }
+ // Was not able to call {@link InputMethodManager#switchToNextInputMethodIBinder,boolean)}
+ // because the current device is running ICS or previous and lacks the API.
+ if (switchToNextInputSubtypeInThisIme(token, onlyCurrentIme)) {
+ return true;
+ }
+ return switchToNextInputMethodAndSubtype(token);
+ }
+
+ private boolean switchToNextInputSubtypeInThisIme(final IBinder token,
+ final boolean onlyCurrentIme) {
+ final InputMethodManager imm = mImmWrapper.mImm;
+ final InputMethodSubtype currentSubtype = imm.getCurrentInputMethodSubtype();
+ final List<InputMethodSubtype> enabledSubtypes = getMyEnabledInputMethodSubtypeList(
+ true /* allowsImplicitlySelectedSubtypes */);
+ final int currentIndex = getSubtypeIndexInList(currentSubtype, enabledSubtypes);
+ if (currentIndex == INDEX_NOT_FOUND) {
+ Log.w(TAG, "Can't find current subtype in enabled subtypes: subtype="
+ + SubtypeLocale.getSubtypeDisplayName(currentSubtype));
+ return false;
+ }
+ final int nextIndex = (currentIndex + 1) % enabledSubtypes.size();
+ if (nextIndex <= currentIndex && !onlyCurrentIme) {
+ // The current subtype is the last or only enabled one and it needs to switch to
+ // next IME.
+ return false;
+ }
+ final InputMethodSubtype nextSubtype = enabledSubtypes.get(nextIndex);
+ setInputMethodAndSubtype(token, nextSubtype);
+ return true;
+ }
+
+ private boolean switchToNextInputMethodAndSubtype(final IBinder token) {
+ final InputMethodManager imm = mImmWrapper.mImm;
+ final List<InputMethodInfo> enabledImis = imm.getEnabledInputMethodList();
+ final int currentIndex = getImiIndexInList(mInputMethodInfoOfThisIme, enabledImis);
+ if (currentIndex == INDEX_NOT_FOUND) {
+ Log.w(TAG, "Can't find current IME in enabled IMEs: IME package="
+ + mInputMethodInfoOfThisIme.getPackageName());
+ return false;
+ }
+ final InputMethodInfo nextImi = getNextNonAuxiliaryIme(currentIndex, enabledImis);
+ final List<InputMethodSubtype> enabledSubtypes = imm.getEnabledInputMethodSubtypeList(
+ nextImi, true /* allowsImplicitlySelectedSubtypes */);
+ if (enabledSubtypes.isEmpty()) {
+ // The next IME has no subtype.
+ imm.setInputMethod(token, nextImi.getId());
+ return true;
+ }
+ final InputMethodSubtype firstSubtype = enabledSubtypes.get(0);
+ imm.setInputMethodAndSubtype(token, nextImi.getId(), firstSubtype);
+ return true;
+ }
+
+ private static int getImiIndexInList(final InputMethodInfo inputMethodInfo,
+ final List<InputMethodInfo> imiList) {
+ final int count = imiList.size();
+ for (int index = 0; index < count; index++) {
+ final InputMethodInfo imi = imiList.get(index);
+ if (imi.equals(inputMethodInfo)) {
+ return index;
+ }
+ }
+ return INDEX_NOT_FOUND;
+ }
+
+ // This method mimics {@link InputMethodManager#switchToNextInputMethod(IBinder,boolean)}.
+ private static InputMethodInfo getNextNonAuxiliaryIme(final int currentIndex,
+ final List<InputMethodInfo> imiList) {
+ final int count = imiList.size();
+ for (int i = 1; i < count; i++) {
+ final int nextIndex = (currentIndex + i) % count;
+ final InputMethodInfo nextImi = imiList.get(nextIndex);
+ if (!isAuxiliaryIme(nextImi)) {
+ return nextImi;
+ }
+ }
+ return imiList.get(currentIndex);
+ }
+
+ // Copied from {@link InputMethodInfo}. See how auxiliary of IME is determined.
+ private static boolean isAuxiliaryIme(final InputMethodInfo imi) {
+ final int count = imi.getSubtypeCount();
+ if (count == 0) {
return false;
}
+ for (int index = 0; index < count; index++) {
+ final InputMethodSubtype subtype = imi.getSubtypeAt(index);
+ if (!subtype.isAuxiliary()) {
+ return false;
+ }
+ }
return true;
}
@@ -122,8 +220,8 @@ public final class RichInputMethodManager {
final InputMethodSubtype subtype) {
final boolean subtypeEnabled = checkIfSubtypeBelongsToThisImeAndEnabled(subtype);
final boolean subtypeExplicitlyEnabled = checkIfSubtypeBelongsToList(
- subtype, mImmWrapper.mImm.getEnabledInputMethodSubtypeList(
- mInputMethodInfoOfThisIme, false /* allowsImplicitlySelectedSubtypes */));
+ subtype, getMyEnabledInputMethodSubtypeList(
+ false /* allowsImplicitlySelectedSubtypes */));
return subtypeEnabled && !subtypeExplicitlyEnabled;
}
@@ -136,24 +234,35 @@ public final class RichInputMethodManager {
private static boolean checkIfSubtypeBelongsToList(final InputMethodSubtype subtype,
final List<InputMethodSubtype> subtypes) {
- for (final InputMethodSubtype ims : subtypes) {
+ return getSubtypeIndexInList(subtype, subtypes) != INDEX_NOT_FOUND;
+ }
+
+ private static int getSubtypeIndexInList(final InputMethodSubtype subtype,
+ final List<InputMethodSubtype> subtypes) {
+ final int count = subtypes.size();
+ for (int index = 0; index < count; index++) {
+ final InputMethodSubtype ims = subtypes.get(index);
if (ims.equals(subtype)) {
- return true;
+ return index;
}
}
- return false;
+ return INDEX_NOT_FOUND;
}
public boolean checkIfSubtypeBelongsToThisIme(final InputMethodSubtype subtype) {
- final InputMethodInfo myImi = mInputMethodInfoOfThisIme;
- final int count = myImi.getSubtypeCount();
- for (int i = 0; i < count; i++) {
- final InputMethodSubtype ims = myImi.getSubtypeAt(i);
+ return getSubtypeIndexInIme(subtype, mInputMethodInfoOfThisIme) != INDEX_NOT_FOUND;
+ }
+
+ private static int getSubtypeIndexInIme(final InputMethodSubtype subtype,
+ final InputMethodInfo imi) {
+ final int count = imi.getSubtypeCount();
+ for (int index = 0; index < count; index++) {
+ final InputMethodSubtype ims = imi.getSubtypeAt(index);
if (ims.equals(subtype)) {
- return true;
+ return index;
}
}
- return false;
+ return INDEX_NOT_FOUND;
}
public InputMethodSubtype getCurrentInputMethodSubtype(
@@ -209,8 +318,7 @@ public final class RichInputMethodManager {
if (filteredImisCount > 1) {
return true;
}
- final List<InputMethodSubtype> subtypes =
- mImmWrapper.mImm.getEnabledInputMethodSubtypeList(null, true);
+ final List<InputMethodSubtype> subtypes = getMyEnabledInputMethodSubtypeList(true);
int keyboardCount = 0;
// imm.getEnabledInputMethodSubtypeList(null, true) will return the current IME's
// both explicitly and implicitly enabled input method subtype.
diff --git a/java/src/com/android/inputmethod/latin/SeekBarDialogPreference.java b/java/src/com/android/inputmethod/latin/SeekBarDialogPreference.java
index 9819a02ef..7c4156c48 100644
--- a/java/src/com/android/inputmethod/latin/SeekBarDialogPreference.java
+++ b/java/src/com/android/inputmethod/latin/SeekBarDialogPreference.java
@@ -59,7 +59,7 @@ public final class SeekBarDialogPreference extends DialogPreference
public void setInterface(final ValueProxy proxy) {
mValueProxy = proxy;
- setSummary(getValueText(proxy.readValue(getKey())));
+ setSummary(getValueText(clipValue(proxy.readValue(getKey()))));
}
private String getValueText(final int value) {
diff --git a/java/src/com/android/inputmethod/latin/Settings.java b/java/src/com/android/inputmethod/latin/Settings.java
index 72e08700a..9fefb58a6 100644
--- a/java/src/com/android/inputmethod/latin/Settings.java
+++ b/java/src/com/android/inputmethod/latin/Settings.java
@@ -36,6 +36,7 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
public static final String PREF_POPUP_ON = "popup_on";
public static final String PREF_VOICE_MODE = "voice_mode";
public static final String PREF_CORRECTION_SETTINGS = "correction_settings";
+ public static final String PREF_EDIT_PERSONAL_DICTIONARY = "edit_personal_dictionary";
public static final String PREF_CONFIGURE_DICTIONARIES_KEY = "configure_dictionaries_key";
public static final String PREF_AUTO_CORRECTION_THRESHOLD = "auto_correction_threshold";
public static final String PREF_SHOW_SUGGESTIONS_SETTING = "show_suggestions_setting";
@@ -46,6 +47,8 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
public static final String PREF_KEY_USE_CONTACTS_DICT = "pref_key_use_contacts_dict";
public static final String PREF_KEY_USE_DOUBLE_SPACE_PERIOD =
"pref_key_use_double_space_period";
+ public static final String PREF_BLOCK_POTENTIALLY_OFFENSIVE =
+ "pref_key_block_potentially_offensive";
public static final String PREF_SHOW_LANGUAGE_SWITCH_KEY =
"pref_show_language_switch_key";
public static final String PREF_INCLUDE_OTHER_IMES_IN_LANGUAGE_SWITCH_LIST =
@@ -78,6 +81,7 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
"pref_suppress_language_switch_key";
public static final String PREF_SEND_FEEDBACK = "send_feedback";
+ public static final String PREF_ABOUT_KEYBOARD = "about_keyboard";
private Resources mRes;
private SharedPreferences mPrefs;
@@ -142,6 +146,10 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
return mCurrentLocale;
}
+ public boolean getBlockPotentiallyOffensive() {
+ return mSettingsValues.mBlockPotentiallyOffensive;
+ }
+
// Accessed from the settings interface, hence public
public static boolean readKeypressSoundEnabled(final SharedPreferences prefs,
final Resources res) {
@@ -163,6 +171,12 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
return !currentAutoCorrectionSetting.equals(autoCorrectionOff);
}
+ public static boolean readBlockPotentiallyOffensive(final SharedPreferences prefs,
+ final Resources res) {
+ return prefs.getBoolean(Settings.PREF_BLOCK_POTENTIALLY_OFFENSIVE,
+ res.getBoolean(R.bool.config_block_potentially_offensive));
+ }
+
public static boolean readFromBuildConfigIfGestureInputEnabled(final Resources res) {
return res.getBoolean(R.bool.config_gesture_input_enabled_by_build_config);
}
diff --git a/java/src/com/android/inputmethod/latin/SettingsFragment.java b/java/src/com/android/inputmethod/latin/SettingsFragment.java
index a96c997c8..835ef7b46 100644
--- a/java/src/com/android/inputmethod/latin/SettingsFragment.java
+++ b/java/src/com/android/inputmethod/latin/SettingsFragment.java
@@ -16,10 +16,13 @@
package com.android.inputmethod.latin;
+import android.app.Activity;
import android.app.backup.BackupManager;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
import android.content.res.Resources;
import android.media.AudioManager;
import android.os.Bundle;
@@ -31,13 +34,19 @@ import android.preference.PreferenceGroup;
import android.preference.PreferenceScreen;
import android.view.inputmethod.InputMethodSubtype;
+import java.util.TreeSet;
+
import com.android.inputmethod.dictionarypack.DictionarySettingsActivity;
import com.android.inputmethod.latin.define.ProductionFlag;
import com.android.inputmethod.latin.setup.LauncherIconVisibilityManager;
+import com.android.inputmethod.latin.userdictionary.UserDictionaryList;
+import com.android.inputmethod.latin.userdictionary.UserDictionarySettings;
import com.android.inputmethodcommon.InputMethodSettingsFragment;
public final class SettingsFragment extends InputMethodSettingsFragment
implements SharedPreferences.OnSharedPreferenceChangeListener {
+ private static final boolean DBG_USE_INTERNAL_USER_DICTIONARY_SETTINGS = false;
+
private ListPreference mVoicePreference;
private ListPreference mShowCorrectionSuggestionsPreference;
private ListPreference mAutoCorrectionThresholdPreference;
@@ -77,10 +86,13 @@ public final class SettingsFragment extends InputMethodSettingsFragment
final Resources res = getResources();
final Context context = getActivity();
- // When we are called from the Settings application but we are not already running, the
- // {@link SubtypeLocale} class may not have been initialized. It is safe to call
- // {@link SubtypeLocale#init(Context)} multiple times.
+ // When we are called from the Settings application but we are not already running, some
+ // singleton and utility classes may not have been initialized. We have to call
+ // initialization method of these classes here. See {@link LatinIME#onCreate()}.
+ SubtypeSwitcher.init(context);
SubtypeLocale.init(context);
+ AudioAndHapticFeedbackManager.init(context);
+
mVoicePreference = (ListPreference) findPreference(Settings.PREF_VOICE_MODE);
mShowCorrectionSuggestionsPreference =
(ListPreference) findPreference(Settings.PREF_SHOW_SUGGESTIONS_SETTING);
@@ -110,6 +122,7 @@ public final class SettingsFragment extends InputMethodSettingsFragment
}
final Preference feedbackSettings = findPreference(Settings.PREF_SEND_FEEDBACK);
+ final Preference aboutSettings = findPreference(Settings.PREF_ABOUT_KEYBOARD);
if (feedbackSettings != null) {
if (FeedbackUtils.isFeedbackFormSupported()) {
feedbackSettings.setOnPreferenceClickListener(new OnPreferenceClickListener() {
@@ -119,8 +132,11 @@ public final class SettingsFragment extends InputMethodSettingsFragment
return true;
}
});
+ aboutSettings.setTitle(FeedbackUtils.getAboutKeyboardTitleResId());
+ aboutSettings.setIntent(FeedbackUtils.getAboutKeyboardIntent(getActivity()));
} else {
miscSettings.removePreference(feedbackSettings);
+ miscSettings.removePreference(aboutSettings);
}
}
@@ -180,6 +196,15 @@ public final class SettingsFragment extends InputMethodSettingsFragment
textCorrectionGroup.removePreference(dictionaryLink);
}
+ final Preference editPersonalDictionary =
+ findPreference(Settings.PREF_EDIT_PERSONAL_DICTIONARY);
+ final Intent editPersonalDictionaryIntent = editPersonalDictionary.getIntent();
+ final ResolveInfo ri = context.getPackageManager().resolveActivity(
+ editPersonalDictionaryIntent, PackageManager.MATCH_DEFAULT_ONLY);
+ if (DBG_USE_INTERNAL_USER_DICTIONARY_SETTINGS || ri == null) {
+ updateUserDictionaryPreference(editPersonalDictionary);
+ }
+
if (!Settings.readFromBuildConfigIfGestureInputEnabled(res)) {
removePreference(Settings.PREF_GESTURE_SETTINGS, getPreferenceScreen());
}
@@ -386,4 +411,28 @@ public final class SettingsFragment extends InputMethodSettingsFragment
}
});
}
+
+ private void updateUserDictionaryPreference(Preference userDictionaryPreference) {
+ final Activity activity = getActivity();
+ final TreeSet<String> localeList = UserDictionaryList.getUserDictionaryLocalesSet(activity);
+ if (null == localeList) {
+ // The locale list is null if and only if the user dictionary service is
+ // not present or disabled. In this case we need to remove the preference.
+ getPreferenceScreen().removePreference(userDictionaryPreference);
+ } else if (localeList.size() <= 1) {
+ userDictionaryPreference.setFragment(UserDictionarySettings.class.getName());
+ // If the size of localeList is 0, we don't set the locale parameter in the
+ // extras. This will be interpreted by the UserDictionarySettings class as
+ // meaning "the current locale".
+ // Note that with the current code for UserDictionaryList#getUserDictionaryLocalesSet()
+ // the locale list always has at least one element, since it always includes the current
+ // locale explicitly. @see UserDictionaryList.getUserDictionaryLocalesSet().
+ if (localeList.size() == 1) {
+ final String locale = (String)localeList.toArray()[0];
+ userDictionaryPreference.getExtras().putString("locale", locale);
+ }
+ } else {
+ userDictionaryPreference.setFragment(UserDictionaryList.class.getName());
+ }
+ }
}
diff --git a/java/src/com/android/inputmethod/latin/SettingsValues.java b/java/src/com/android/inputmethod/latin/SettingsValues.java
index f77a92885..615b2dfab 100644
--- a/java/src/com/android/inputmethod/latin/SettingsValues.java
+++ b/java/src/com/android/inputmethod/latin/SettingsValues.java
@@ -34,6 +34,9 @@ import java.util.Arrays;
*/
public final class SettingsValues {
private static final String TAG = SettingsValues.class.getSimpleName();
+ // "floatNegativeInfinity" is a special marker string for Float.NEGATIVE_INFINITE
+ // currently used for auto-correction
+ private static final String FLOAT_NEGATIVE_INFINITY_MARKER_STRING = "floatNegativeInfinity";
// From resources:
public final int mDelayUpdateOldSuggestions;
@@ -54,6 +57,7 @@ public final class SettingsValues {
public final boolean mShowsLanguageSwitchKey;
public final boolean mUseContactsDict;
public final boolean mUseDoubleSpacePeriod;
+ public final boolean mBlockPotentiallyOffensive;
// Use bigrams to predict the next word when there is no input for it yet
public final boolean mBigramPredictionEnabled;
public final boolean mGestureInputEnabled;
@@ -123,6 +127,7 @@ public final class SettingsValues {
mShowsLanguageSwitchKey = Settings.readShowsLanguageSwitchKey(prefs);
mUseContactsDict = prefs.getBoolean(Settings.PREF_KEY_USE_CONTACTS_DICT, true);
mUseDoubleSpacePeriod = prefs.getBoolean(Settings.PREF_KEY_USE_DOUBLE_SPACE_PERIOD, true);
+ mBlockPotentiallyOffensive = Settings.readBlockPotentiallyOffensive(prefs, res);
mAutoCorrectEnabled = Settings.readAutoCorrectEnabled(autoCorrectionThresholdRawValue, res);
mBigramPredictionEnabled = readBigramPredictionEnabled(prefs, res);
@@ -266,8 +271,12 @@ public final class SettingsValues {
try {
final int arrayIndex = Integer.valueOf(currentAutoCorrectionSetting);
if (arrayIndex >= 0 && arrayIndex < autoCorrectionThresholdValues.length) {
- autoCorrectionThreshold = Float.parseFloat(
- autoCorrectionThresholdValues[arrayIndex]);
+ final String val = autoCorrectionThresholdValues[arrayIndex];
+ if (FLOAT_NEGATIVE_INFINITY_MARKER_STRING.equals(val)) {
+ autoCorrectionThreshold = Float.NEGATIVE_INFINITY;
+ } else {
+ autoCorrectionThreshold = Float.parseFloat(val);
+ }
}
} catch (NumberFormatException e) {
// Whenever the threshold settings are correct, never come here.
@@ -275,7 +284,7 @@ public final class SettingsValues {
Log.w(TAG, "Cannot load auto correction threshold setting."
+ " currentAutoCorrectionSetting: " + currentAutoCorrectionSetting
+ ", autoCorrectionThresholdValues: "
- + Arrays.toString(autoCorrectionThresholdValues));
+ + Arrays.toString(autoCorrectionThresholdValues), e);
}
return autoCorrectionThreshold;
}
diff --git a/java/src/com/android/inputmethod/latin/StringUtils.java b/java/src/com/android/inputmethod/latin/StringUtils.java
index d5ee58a63..ab050d7a3 100644
--- a/java/src/com/android/inputmethod/latin/StringUtils.java
+++ b/java/src/com/android/inputmethod/latin/StringUtils.java
@@ -65,23 +65,6 @@ public final class StringUtils {
}
/**
- * Find a string that start with specified prefix from an array.
- *
- * @param prefix a prefix string to find.
- * @param array an string array to be searched.
- * @return the rest part of the string that starts with the prefix.
- * Returns null if it couldn't be found.
- */
- public static String findPrefixedString(final String prefix, final String[] array) {
- for (final String element : array) {
- if (element.startsWith(prefix)) {
- return element.substring(prefix.length());
- }
- }
- return null;
- }
-
- /**
* Remove duplicates from an array of strings.
*
* This method will always keep the first occurrence of all strings at their position
diff --git a/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java b/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java
index 2f9e34ff1..282b5794f 100644
--- a/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java
+++ b/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java
@@ -80,6 +80,7 @@ public final class SubtypeSwitcher {
public static void init(final Context context) {
SubtypeLocale.init(context);
+ RichInputMethodManager.init(context);
sInstance.initialize(context);
}
@@ -87,10 +88,13 @@ public final class SubtypeSwitcher {
// Intentional empty constructor for singleton.
}
- private void initialize(final Context service) {
- mResources = service.getResources();
+ private void initialize(final Context context) {
+ if (mResources != null) {
+ return;
+ }
+ mResources = context.getResources();
mRichImm = RichInputMethodManager.getInstance();
- mConnectivityManager = (ConnectivityManager) service.getSystemService(
+ mConnectivityManager = (ConnectivityManager) context.getSystemService(
Context.CONNECTIVITY_SERVICE);
mNoLanguageSubtype = mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet(
SubtypeLocale.NO_LANGUAGE, SubtypeLocale.QWERTY);
@@ -111,7 +115,7 @@ public final class SubtypeSwitcher {
*/
public void updateParametersOnStartInputView() {
final List<InputMethodSubtype> enabledSubtypesOfThisIme =
- mRichImm.getInputMethodManager().getEnabledInputMethodSubtypeList(null, true);
+ mRichImm.getMyEnabledInputMethodSubtypeList(true);
mNeedsToDisplayLanguage.updateEnabledSubtypeCount(enabledSubtypesOfThisIme.size());
updateShortcutIME();
}
diff --git a/java/src/com/android/inputmethod/latin/Suggest.java b/java/src/com/android/inputmethod/latin/Suggest.java
index 59d0207f6..dc9bef22a 100644
--- a/java/src/com/android/inputmethod/latin/Suggest.java
+++ b/java/src/com/android/inputmethod/latin/Suggest.java
@@ -174,21 +174,22 @@ public final class Suggest {
public SuggestedWords getSuggestedWords(final WordComposer wordComposer,
final String prevWordForBigram, final ProximityInfo proximityInfo,
- final boolean isCorrectionEnabled, final int sessionId) {
+ final boolean blockOffensiveWords, final boolean isCorrectionEnabled,
+ final int sessionId) {
LatinImeLogger.onStartSuggestion(prevWordForBigram);
if (wordComposer.isBatchMode()) {
return getSuggestedWordsForBatchInput(
- wordComposer, prevWordForBigram, proximityInfo, sessionId);
+ wordComposer, prevWordForBigram, proximityInfo, blockOffensiveWords, sessionId);
} else {
return getSuggestedWordsForTypingInput(wordComposer, prevWordForBigram, proximityInfo,
- isCorrectionEnabled);
+ blockOffensiveWords, isCorrectionEnabled);
}
}
// Retrieves suggestions for the typing input.
private SuggestedWords getSuggestedWordsForTypingInput(final WordComposer wordComposer,
final String prevWordForBigram, final ProximityInfo proximityInfo,
- final boolean isCorrectionEnabled) {
+ final boolean blockOffensiveWords, final boolean isCorrectionEnabled) {
final int trailingSingleQuotesCount = wordComposer.trailingSingleQuotesCount();
final BoundedTreeSet suggestionsSet = new BoundedTreeSet(sSuggestedWordInfoComparator,
MAX_SUGGESTIONS);
@@ -212,7 +213,7 @@ public final class Suggest {
for (final String key : mDictionaries.keySet()) {
final Dictionary dictionary = mDictionaries.get(key);
suggestionsSet.addAll(dictionary.getSuggestions(
- wordComposerForLookup, prevWordForBigram, proximityInfo));
+ wordComposerForLookup, prevWordForBigram, proximityInfo, blockOffensiveWords));
}
final String whitelistedWord;
@@ -301,7 +302,7 @@ public final class Suggest {
// Retrieves suggestions for the batch input.
private SuggestedWords getSuggestedWordsForBatchInput(final WordComposer wordComposer,
final String prevWordForBigram, final ProximityInfo proximityInfo,
- final int sessionId) {
+ final boolean blockOffensiveWords, final int sessionId) {
final BoundedTreeSet suggestionsSet = new BoundedTreeSet(sSuggestedWordInfoComparator,
MAX_SUGGESTIONS);
@@ -314,8 +315,8 @@ public final class Suggest {
continue;
}
final Dictionary dictionary = mDictionaries.get(key);
- suggestionsSet.addAll(dictionary.getSuggestionsWithSessionId(
- wordComposer, prevWordForBigram, proximityInfo, sessionId));
+ suggestionsSet.addAll(dictionary.getSuggestionsWithSessionId(wordComposer,
+ prevWordForBigram, proximityInfo, blockOffensiveWords, sessionId));
}
for (SuggestedWordInfo wordInfo : suggestionsSet) {
diff --git a/java/src/com/android/inputmethod/latin/SuggestedWords.java b/java/src/com/android/inputmethod/latin/SuggestedWords.java
index 616e1911b..dfddb0ffe 100644
--- a/java/src/com/android/inputmethod/latin/SuggestedWords.java
+++ b/java/src/com/android/inputmethod/latin/SuggestedWords.java
@@ -122,6 +122,7 @@ public final class SuggestedWords {
public static final class SuggestedWordInfo {
public static final int MAX_SCORE = Integer.MAX_VALUE;
+ public static final int KIND_MASK_KIND = 0xFF; // Mask to get only the kind
public static final int KIND_TYPED = 0; // What user typed
public static final int KIND_CORRECTION = 1; // Simple correction/suggestion
public static final int KIND_COMPLETION = 2; // Completion (suggestion with appended chars)
@@ -132,6 +133,11 @@ public final class SuggestedWords {
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)
+
+ public static final int KIND_MASK_FLAGS = 0xFFFFFF00; // Mask to get the flags
+ public static final int KIND_FLAG_POSSIBLY_OFFENSIVE = 0x80000000;
+ public static final int KIND_FLAG_EXACT_MATCH = 0x40000000;
+
public final String mWord;
public final int mScore;
public final int mKind; // one of the KIND_* constants above
diff --git a/java/src/com/android/inputmethod/latin/SynchronouslyLoadedContactsBinaryDictionary.java b/java/src/com/android/inputmethod/latin/SynchronouslyLoadedContactsBinaryDictionary.java
index ec4dc1436..92f96c027 100644
--- a/java/src/com/android/inputmethod/latin/SynchronouslyLoadedContactsBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/SynchronouslyLoadedContactsBinaryDictionary.java
@@ -33,9 +33,10 @@ public final class SynchronouslyLoadedContactsBinaryDictionary extends ContactsB
@Override
public synchronized ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer codes,
- final String prevWordForBigrams, final ProximityInfo proximityInfo) {
+ final String prevWordForBigrams, final ProximityInfo proximityInfo,
+ final boolean blockOffensiveWords) {
syncReloadDictionaryIfRequired();
- return super.getSuggestions(codes, prevWordForBigrams, proximityInfo);
+ return super.getSuggestions(codes, prevWordForBigrams, proximityInfo, blockOffensiveWords);
}
@Override
diff --git a/java/src/com/android/inputmethod/latin/SynchronouslyLoadedUserBinaryDictionary.java b/java/src/com/android/inputmethod/latin/SynchronouslyLoadedUserBinaryDictionary.java
index 4bdaf2039..33fe89611 100644
--- a/java/src/com/android/inputmethod/latin/SynchronouslyLoadedUserBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/SynchronouslyLoadedUserBinaryDictionary.java
@@ -36,9 +36,10 @@ public final class SynchronouslyLoadedUserBinaryDictionary extends UserBinaryDic
@Override
public synchronized ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer codes,
- final String prevWordForBigrams, final ProximityInfo proximityInfo) {
+ final String prevWordForBigrams, final ProximityInfo proximityInfo,
+ final boolean blockOffensiveWords) {
syncReloadDictionaryIfRequired();
- return super.getSuggestions(codes, prevWordForBigrams, proximityInfo);
+ return super.getSuggestions(codes, prevWordForBigrams, proximityInfo, blockOffensiveWords);
}
@Override
diff --git a/java/src/com/android/inputmethod/latin/Utils.java b/java/src/com/android/inputmethod/latin/Utils.java
index aff5d17d7..0f96c54dc 100644
--- a/java/src/com/android/inputmethod/latin/Utils.java
+++ b/java/src/com/android/inputmethod/latin/Utils.java
@@ -21,6 +21,7 @@ import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
+import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.inputmethodservice.InputMethodService;
@@ -473,4 +474,18 @@ public final class Utils {
}
return 0;
}
+
+ public static String getVersionName(Context context) {
+ try {
+ if (context == null) {
+ return "";
+ }
+ final String packageName = context.getPackageName();
+ PackageInfo info = context.getPackageManager().getPackageInfo(packageName, 0);
+ return info.versionName;
+ } catch (NameNotFoundException e) {
+ Log.e(TAG, "Could not find version info.", e);
+ }
+ return "";
+ }
}
diff --git a/java/src/com/android/inputmethod/latin/WordComposer.java b/java/src/com/android/inputmethod/latin/WordComposer.java
index 51bd901fb..e078f03f4 100644
--- a/java/src/com/android/inputmethod/latin/WordComposer.java
+++ b/java/src/com/android/inputmethod/latin/WordComposer.java
@@ -16,7 +16,6 @@
package com.android.inputmethod.latin;
-import com.android.inputmethod.annotations.UsedForTesting;
import com.android.inputmethod.keyboard.Key;
import com.android.inputmethod.keyboard.Keyboard;
@@ -211,9 +210,8 @@ public final class WordComposer {
}
/**
- * Internal method to retrieve reasonable proximity info for a character.
+ * Add a dummy key by retrieving reasonable coordinates
*/
- @UsedForTesting
public void addKeyInfo(final int codePoint, final Keyboard keyboard) {
final int x, y;
final Key key;
diff --git a/java/src/com/android/inputmethod/latin/personalization/AccountUtils.java b/java/src/com/android/inputmethod/latin/personalization/AccountUtils.java
new file mode 100644
index 000000000..93687e193
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/personalization/AccountUtils.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.personalization;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.content.Context;
+import android.util.Patterns;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class AccountUtils {
+ private AccountUtils() {
+ // This utility class is not publicly instantiable.
+ }
+
+ private static Account[] getAccounts(final Context context) {
+ return AccountManager.get(context).getAccounts();
+ }
+
+ public static List<String> getDeviceAccountsEmailAddresses(final Context context) {
+ final ArrayList<String> retval = new ArrayList<String>();
+ for (final Account account : getAccounts(context)) {
+ final String name = account.name;
+ if (Patterns.EMAIL_ADDRESS.matcher(name).matches()) {
+ retval.add(name);
+ retval.add(name.split("@")[0]);
+ }
+ }
+ return retval;
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/setup/SetupActivity.java b/java/src/com/android/inputmethod/latin/setup/SetupActivity.java
index 15d0bac37..8a2de887d 100644
--- a/java/src/com/android/inputmethod/latin/setup/SetupActivity.java
+++ b/java/src/com/android/inputmethod/latin/setup/SetupActivity.java
@@ -19,172 +19,25 @@ package com.android.inputmethod.latin.setup;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
-import android.content.res.Resources;
-import android.graphics.PorterDuff;
-import android.graphics.drawable.Drawable;
import android.os.Bundle;
-import android.os.Message;
import android.provider.Settings;
-import android.view.View;
import android.view.inputmethod.InputMethodInfo;
import android.view.inputmethod.InputMethodManager;
-import android.widget.TextView;
-import com.android.inputmethod.compat.TextViewCompatUtils;
-import com.android.inputmethod.compat.ViewCompatUtils;
-import com.android.inputmethod.latin.CollectionUtils;
-import com.android.inputmethod.latin.R;
import com.android.inputmethod.latin.RichInputMethodManager;
-import com.android.inputmethod.latin.SettingsActivity;
-import com.android.inputmethod.latin.StaticInnerHandlerWrapper;
-
-import java.util.HashMap;
public final class SetupActivity extends Activity {
- private SetupStepIndicatorView mStepIndicatorView;
- private final SetupStepGroup mSetupSteps = new SetupStepGroup();
- private static final String STATE_STEP = "step";
- private int mStepNumber;
- private static final int STEP_1 = 1;
- private static final int STEP_2 = 2;
- private static final int STEP_3 = 3;
-
- private final SettingsPoolingHandler mHandler = new SettingsPoolingHandler(this);
-
- static final class SettingsPoolingHandler extends StaticInnerHandlerWrapper<SetupActivity> {
- private static final int MSG_POLLING_IME_SETTINGS = 0;
- private static final long IME_SETTINGS_POLLING_INTERVAL = 200;
-
- public SettingsPoolingHandler(final SetupActivity outerInstance) {
- super(outerInstance);
- }
-
- @Override
- public void handleMessage(final Message msg) {
- final SetupActivity setupActivity = getOuterInstance();
- if (setupActivity == null) {
- return;
- }
- switch (msg.what) {
- case MSG_POLLING_IME_SETTINGS:
- if (SetupActivity.isThisImeEnabled(setupActivity)) {
- setupActivity.invokeSetupWizardOfThisIme();
- return;
- }
- startPollingImeSettings();
- break;
- }
- }
-
- public void startPollingImeSettings() {
- sendMessageDelayed(obtainMessage(MSG_POLLING_IME_SETTINGS),
- IME_SETTINGS_POLLING_INTERVAL);
- }
-
- public void cancelPollingImeSettings() {
- removeMessages(MSG_POLLING_IME_SETTINGS);
- }
- }
-
@Override
protected void onCreate(final Bundle savedInstanceState) {
- setTheme(android.R.style.Theme_DeviceDefault_Light_NoActionBar);
super.onCreate(savedInstanceState);
-
- setContentView(R.layout.setup_wizard);
-
- RichInputMethodManager.init(this);
-
- if (savedInstanceState == null) {
- mStepNumber = determineSetupStepNumber();
- } else {
- mStepNumber = savedInstanceState.getInt(STATE_STEP);
- }
-
- if (mStepNumber == STEP_3) {
- // This IME already has been enabled and set as current IME.
- // TODO: Implement tutorial.
- invokeSettingsOfThisIme();
- finish();
- return;
- }
-
- // TODO: Use sans-serif-thin font family depending on the system locale white list and
- // the SDK version.
- final TextView titleView = (TextView)findViewById(R.id.setup_title);
- final int appName = getApplicationInfo().labelRes;
- titleView.setText(getString(R.string.setup_title, getString(appName)));
-
- mStepIndicatorView = (SetupStepIndicatorView)findViewById(R.id.setup_step_indicator);
-
- final SetupStep step1 = new SetupStep(findViewById(R.id.setup_step1),
- appName, R.string.setup_step1_title, R.string.setup_step1_instruction,
- R.drawable.ic_settings_language, R.string.language_settings);
- step1.setAction(new Runnable() {
- @Override
- public void run() {
- invokeLanguageAndInputSettings();
- mHandler.startPollingImeSettings();
- }
- });
- mSetupSteps.addStep(STEP_1, step1);
-
- final SetupStep step2 = new SetupStep(findViewById(R.id.setup_step2),
- appName, R.string.setup_step2_title, R.string.setup_step2_instruction,
- 0 /* actionIcon */, R.string.select_input_method);
- step2.setAction(new Runnable() {
- @Override
- public void run() {
- // Invoke input method picker.
- RichInputMethodManager.getInstance().getInputMethodManager()
- .showInputMethodPicker();
- }
- });
- mSetupSteps.addStep(STEP_2, step2);
-
- final SetupStep step3 = new SetupStep(findViewById(R.id.setup_step3),
- appName, R.string.setup_step3_title, 0 /* instruction */,
- R.drawable.sym_keyboard_language_switch, R.string.setup_step3_instruction);
- step3.setAction(new Runnable() {
- @Override
- public void run() {
- invokeSubtypeEnablerOfThisIme();
- }
- });
- mSetupSteps.addStep(STEP_3, step3);
- }
-
- private void invokeSetupWizardOfThisIme() {
final Intent intent = new Intent();
- intent.setClass(this, SetupActivity.class);
- intent.setFlags(Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
- | Intent.FLAG_ACTIVITY_CLEAR_TOP);
- startActivity(intent);
- }
-
- private void invokeSettingsOfThisIme() {
- final Intent intent = new Intent();
- intent.setClass(this, SettingsActivity.class);
- intent.setFlags(Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
- | Intent.FLAG_ACTIVITY_CLEAR_TOP);
- startActivity(intent);
- }
-
- private void invokeLanguageAndInputSettings() {
- final Intent intent = new Intent();
- intent.setAction(Settings.ACTION_INPUT_METHOD_SETTINGS);
- intent.addCategory(Intent.CATEGORY_DEFAULT);
- startActivity(intent);
- }
-
- private void invokeSubtypeEnablerOfThisIme() {
- final InputMethodInfo imi =
- RichInputMethodManager.getInstance().getInputMethodInfoOfThisIme();
- final Intent intent = new Intent();
- intent.setAction(Settings.ACTION_INPUT_METHOD_SUBTYPE_SETTINGS);
- intent.addCategory(Intent.CATEGORY_DEFAULT);
- intent.putExtra(Settings.EXTRA_INPUT_METHOD_ID, imi.getId());
+ intent.setClass(this, SetupWizardActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP
+ | Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
+ if (!isFinishing()) {
+ finish();
+ }
}
/**
@@ -221,141 +74,4 @@ public final class SetupActivity extends Activity {
context.getContentResolver(), Settings.Secure.DEFAULT_INPUT_METHOD);
return myImi.getId().equals(currentImeId);
}
-
- private int determineSetupStepNumber() {
- mHandler.cancelPollingImeSettings();
- if (!isThisImeEnabled(this)) {
- return STEP_1;
- }
- if (!isThisImeCurrent(this)) {
- return STEP_2;
- }
- return STEP_3;
- }
-
- @Override
- protected void onSaveInstanceState(final Bundle outState) {
- super.onSaveInstanceState(outState);
- outState.putInt(STATE_STEP, mStepNumber);
- }
-
- @Override
- protected void onRestoreInstanceState(final Bundle savedInstanceState) {
- super.onRestoreInstanceState(savedInstanceState);
- mStepNumber = savedInstanceState.getInt(STATE_STEP);
- }
-
- @Override
- protected void onStart() {
- super.onStart();
- mStepNumber = determineSetupStepNumber();
- }
-
- @Override
- protected void onRestart() {
- super.onRestart();
- mStepNumber = determineSetupStepNumber();
- }
-
- @Override
- protected void onResume() {
- super.onResume();
- updateSetupStepView();
- }
-
- @Override
- public void onWindowFocusChanged(final boolean hasFocus) {
- super.onWindowFocusChanged(hasFocus);
- if (!hasFocus) {
- return;
- }
- mStepNumber = determineSetupStepNumber();
- updateSetupStepView();
- }
-
- private void updateSetupStepView() {
- final int layoutDirection = ViewCompatUtils.getLayoutDirection(mStepIndicatorView);
- mStepIndicatorView.setIndicatorPosition(
- getIndicatorPosition(mStepNumber, mSetupSteps.getTotalStep(), layoutDirection));
- mSetupSteps.enableStep(mStepNumber);
- }
-
- private static float getIndicatorPosition(final int step, final int totalStep,
- final int layoutDirection) {
- final float pos = ((step - STEP_1) * 2 + 1) / (float)(totalStep * 2);
- return (layoutDirection == ViewCompatUtils.LAYOUT_DIRECTION_RTL) ? 1.0f - pos : pos;
- }
-
- static final class SetupStep implements View.OnClickListener {
- private final View mRootView;
- private final TextView mActionLabel;
- private Runnable mAction;
-
- public SetupStep(final View rootView, final int appName, final int title,
- final int instruction, final int actionIcon, final int actionLabel) {
- mRootView = rootView;
- final Resources res = rootView.getResources();
- final String applicationName = res.getString(appName);
-
- final TextView titleView = (TextView)rootView.findViewById(R.id.setup_step_title);
- titleView.setText(res.getString(title, applicationName));
-
- final TextView instructionView = (TextView)rootView.findViewById(
- R.id.setup_step_instruction);
- if (instruction == 0) {
- instructionView.setVisibility(View.GONE);
- } else {
- instructionView.setText(res.getString(instruction, applicationName));
- }
-
- mActionLabel = (TextView)rootView.findViewById(R.id.setup_step_action_label);
- mActionLabel.setText(res.getString(actionLabel));
- if (actionIcon == 0) {
- final int paddingEnd = ViewCompatUtils.getPaddingEnd(mActionLabel);
- ViewCompatUtils.setPaddingRelative(mActionLabel, paddingEnd, 0, paddingEnd, 0);
- } else {
- final int overrideColor = res.getColor(R.color.setup_text_action);
- final Drawable icon = res.getDrawable(actionIcon);
- icon.setColorFilter(overrideColor, PorterDuff.Mode.MULTIPLY);
- icon.setBounds(0, 0, icon.getIntrinsicWidth(), icon.getIntrinsicHeight());
- TextViewCompatUtils.setCompoundDrawablesRelative(
- mActionLabel, icon, null, null, null);
- }
- }
-
- public void setEnabled(final boolean enabled) {
- mRootView.setVisibility(enabled ? View.VISIBLE : View.GONE);
- }
-
- public void setAction(final Runnable action) {
- mActionLabel.setOnClickListener(this);
- mAction = action;
- }
-
- @Override
- public void onClick(final View v) {
- if (mAction != null) {
- mAction.run();
- }
- }
- }
-
- static final class SetupStepGroup {
- private final HashMap<Integer, SetupStep> mGroup = CollectionUtils.newHashMap();
-
- public void addStep(final int stepNo, final SetupStep step) {
- mGroup.put(stepNo, step);
- }
-
- public void enableStep(final int enableStepNo) {
- for (final Integer stepNo : mGroup.keySet()) {
- final SetupStep step = mGroup.get(stepNo);
- step.setEnabled(stepNo == enableStepNo);
- }
- }
-
- public int getTotalStep() {
- return mGroup.size();
- }
- }
}
diff --git a/java/src/com/android/inputmethod/latin/setup/SetupStartIndicatorView.java b/java/src/com/android/inputmethod/latin/setup/SetupStartIndicatorView.java
new file mode 100644
index 000000000..974dfddd3
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/setup/SetupStartIndicatorView.java
@@ -0,0 +1,123 @@
+/*
+ * 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.setup;
+
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.android.inputmethod.compat.ViewCompatUtils;
+import com.android.inputmethod.latin.R;
+
+public final class SetupStartIndicatorView extends LinearLayout {
+ public SetupStartIndicatorView(final Context context, final AttributeSet attrs) {
+ super(context, attrs);
+ setOrientation(HORIZONTAL);
+ LayoutInflater.from(context).inflate(R.layout.setup_start_indicator_label, this);
+
+ final LabelView labelView = (LabelView)findViewById(R.id.setup_start_label);
+ labelView.setIndicatorView(findViewById(R.id.setup_start_indicator));
+ }
+
+ public static final class LabelView extends TextView {
+ private View mIndicatorView;
+
+ public LabelView(final Context context, final AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public void setIndicatorView(final View indicatorView) {
+ mIndicatorView = indicatorView;
+ }
+
+ // TODO: Once we stop supporting ICS, uncomment {@link #setPressed(boolean)} method and
+ // remove this method.
+ @Override
+ protected void drawableStateChanged() {
+ super.drawableStateChanged();
+ for (final int state : getDrawableState()) {
+ if (state == android.R.attr.state_pressed) {
+ updateIndicatorView(true /* pressed */);
+ return;
+ }
+ }
+ updateIndicatorView(false /* pressed */);
+ }
+
+ // TODO: Once we stop supporting ICS, uncomment this method and remove
+ // {@link #drawableStateChanged()} method.
+// @Override
+// public void setPressed(final boolean pressed) {
+// super.setPressed(pressed);
+// updateIndicatorView(pressed);
+// }
+
+ private void updateIndicatorView(final boolean pressed) {
+ if (mIndicatorView != null) {
+ mIndicatorView.setPressed(pressed);
+ mIndicatorView.invalidate();
+ }
+ }
+ }
+
+ public static final class IndicatorView extends View {
+ private final Path mIndicatorPath = new Path();
+ private final Paint mIndicatorPaint = new Paint();
+ private final ColorStateList mIndicatorColor;
+
+ public IndicatorView(final Context context, final AttributeSet attrs) {
+ super(context, attrs);
+ mIndicatorColor = getResources().getColorStateList(
+ R.color.setup_step_action_background);
+ mIndicatorPaint.setStyle(Paint.Style.FILL);
+ }
+
+ @Override
+ protected void onDraw(final Canvas canvas) {
+ super.onDraw(canvas);
+ final int layoutDirection = ViewCompatUtils.getLayoutDirection(this);
+ final int width = getWidth();
+ final int height = getHeight();
+ final float halfHeight = height / 2.0f;
+ final Path path = mIndicatorPath;
+ path.rewind();
+ if (layoutDirection == ViewCompatUtils.LAYOUT_DIRECTION_RTL) {
+ // Left arrow
+ path.moveTo(width, 0.0f);
+ path.lineTo(0.0f, halfHeight);
+ path.lineTo(width, height);
+ } else { // LAYOUT_DIRECTION_LTR
+ // Right arrow
+ path.moveTo(0.0f, 0.0f);
+ path.lineTo(width, halfHeight);
+ path.lineTo(0.0f, height);
+ }
+ path.close();
+ final int[] stateSet = getDrawableState();
+ final int color = mIndicatorColor.getColorForState(stateSet, 0);
+ mIndicatorPaint.setColor(color);
+ canvas.drawPath(path, mIndicatorPaint);
+ }
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/setup/SetupStepIndicatorView.java b/java/src/com/android/inputmethod/latin/setup/SetupStepIndicatorView.java
index 077a21793..c909507c6 100644
--- a/java/src/com/android/inputmethod/latin/setup/SetupStepIndicatorView.java
+++ b/java/src/com/android/inputmethod/latin/setup/SetupStepIndicatorView.java
@@ -23,6 +23,7 @@ import android.graphics.Path;
import android.util.AttributeSet;
import android.view.View;
+import com.android.inputmethod.compat.ViewCompatUtils;
import com.android.inputmethod.latin.R;
public final class SetupStepIndicatorView extends View {
@@ -36,8 +37,13 @@ public final class SetupStepIndicatorView extends View {
mIndicatorPaint.setStyle(Paint.Style.FILL);
}
- public void setIndicatorPosition(final float xRatio) {
- mXRatio = xRatio;
+ public void setIndicatorPosition(final int stepPos, final int totalStepNum) {
+ final int layoutDirection = ViewCompatUtils.getLayoutDirection(this);
+ // The indicator position is the center of the partition that is equally divided into
+ // the total step number.
+ final float partionWidth = 1.0f / totalStepNum;
+ final float pos = stepPos * partionWidth + partionWidth / 2.0f;
+ mXRatio = (layoutDirection == ViewCompatUtils.LAYOUT_DIRECTION_RTL) ? 1.0f - pos : pos;
invalidate();
}
diff --git a/java/src/com/android/inputmethod/latin/setup/SetupWizardActivity.java b/java/src/com/android/inputmethod/latin/setup/SetupWizardActivity.java
new file mode 100644
index 000000000..78a6478c6
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/setup/SetupWizardActivity.java
@@ -0,0 +1,493 @@
+/*
+ * 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.setup;
+
+import android.app.Activity;
+import android.content.ContentResolver;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.media.MediaPlayer;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Message;
+import android.provider.Settings;
+import android.util.Log;
+import android.view.View;
+import android.view.inputmethod.InputMethodInfo;
+import android.widget.ImageView;
+import android.widget.TextView;
+import android.widget.VideoView;
+
+import com.android.inputmethod.compat.TextViewCompatUtils;
+import com.android.inputmethod.compat.ViewCompatUtils;
+import com.android.inputmethod.latin.CollectionUtils;
+import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.RichInputMethodManager;
+import com.android.inputmethod.latin.SettingsActivity;
+import com.android.inputmethod.latin.StaticInnerHandlerWrapper;
+
+import java.util.ArrayList;
+
+// TODO: Use Fragment to implement welcome screen and setup steps.
+public final class SetupWizardActivity extends Activity implements View.OnClickListener {
+ static final String TAG = SetupWizardActivity.class.getSimpleName();
+
+ private static final boolean ENABLE_WELCOME_VIDEO = true;
+
+ private View mSetupWizard;
+ private View mWelcomeScreen;
+ private View mSetupScreen;
+ private Uri mWelcomeVideoUri;
+ private VideoView mWelcomeVideoView;
+ private ImageView mWelcomeImageView;
+ private View mActionStart;
+ private View mActionNext;
+ private TextView mStep1Bullet;
+ private TextView mActionFinish;
+ private SetupStepGroup mSetupStepGroup;
+ private static final String STATE_STEP = "step";
+ private int mStepNumber;
+ private boolean mNeedsToAdjustStepNumberToSystemState;
+ private static final int STEP_WELCOME = 0;
+ private static final int STEP_1 = 1;
+ private static final int STEP_2 = 2;
+ private static final int STEP_3 = 3;
+ private static final int STEP_LAUNCHING_IME_SETTINGS = 4;
+ private static final int STEP_BACK_FROM_IME_SETTINGS = 5;
+
+ final SettingsPoolingHandler mHandler = new SettingsPoolingHandler(this);
+
+ static final class SettingsPoolingHandler
+ extends StaticInnerHandlerWrapper<SetupWizardActivity> {
+ private static final int MSG_POLLING_IME_SETTINGS = 0;
+ private static final long IME_SETTINGS_POLLING_INTERVAL = 200;
+
+ public SettingsPoolingHandler(final SetupWizardActivity outerInstance) {
+ super(outerInstance);
+ }
+
+ @Override
+ public void handleMessage(final Message msg) {
+ final SetupWizardActivity setupWizardActivity = getOuterInstance();
+ if (setupWizardActivity == null) {
+ return;
+ }
+ switch (msg.what) {
+ case MSG_POLLING_IME_SETTINGS:
+ if (SetupActivity.isThisImeEnabled(setupWizardActivity)) {
+ setupWizardActivity.invokeSetupWizardOfThisIme();
+ return;
+ }
+ startPollingImeSettings();
+ break;
+ }
+ }
+
+ public void startPollingImeSettings() {
+ sendMessageDelayed(obtainMessage(MSG_POLLING_IME_SETTINGS),
+ IME_SETTINGS_POLLING_INTERVAL);
+ }
+
+ public void cancelPollingImeSettings() {
+ removeMessages(MSG_POLLING_IME_SETTINGS);
+ }
+ }
+
+ @Override
+ protected void onCreate(final Bundle savedInstanceState) {
+ setTheme(android.R.style.Theme_Translucent_NoTitleBar);
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.setup_wizard);
+ mSetupWizard = findViewById(R.id.setup_wizard);
+
+ RichInputMethodManager.init(this);
+
+ if (savedInstanceState == null) {
+ mStepNumber = determineSetupStepNumberFromLauncher();
+ } else {
+ mStepNumber = savedInstanceState.getInt(STATE_STEP);
+ }
+
+ final String applicationName = getResources().getString(getApplicationInfo().labelRes);
+ mWelcomeScreen = findViewById(R.id.setup_welcome_screen);
+ final TextView welcomeTitle = (TextView)findViewById(R.id.setup_welcome_title);
+ welcomeTitle.setText(getString(R.string.setup_welcome_title, applicationName));
+
+ mSetupScreen = findViewById(R.id.setup_steps_screen);
+ final TextView stepsTitle = (TextView)findViewById(R.id.setup_title);
+ stepsTitle.setText(getString(R.string.setup_steps_title, applicationName));
+
+ final SetupStepIndicatorView indicatorView =
+ (SetupStepIndicatorView)findViewById(R.id.setup_step_indicator);
+ mSetupStepGroup = new SetupStepGroup(indicatorView);
+
+ mStep1Bullet = (TextView)findViewById(R.id.setup_step1_bullet);
+ mStep1Bullet.setOnClickListener(this);
+ final SetupStep step1 = new SetupStep(STEP_1, applicationName,
+ mStep1Bullet, findViewById(R.id.setup_step1),
+ R.string.setup_step1_title, R.string.setup_step1_instruction,
+ R.string.setup_step1_finished_instruction, R.drawable.ic_setup_step1,
+ R.string.setup_step1_action);
+ step1.setAction(new Runnable() {
+ @Override
+ public void run() {
+ invokeLanguageAndInputSettings();
+ mHandler.startPollingImeSettings();
+ }
+ });
+ mSetupStepGroup.addStep(step1);
+
+ final SetupStep step2 = new SetupStep(STEP_2, applicationName,
+ (TextView)findViewById(R.id.setup_step2_bullet), findViewById(R.id.setup_step2),
+ R.string.setup_step2_title, R.string.setup_step2_instruction,
+ 0 /* finishedInstruction */, R.drawable.ic_setup_step2,
+ R.string.setup_step2_action);
+ step2.setAction(new Runnable() {
+ @Override
+ public void run() {
+ invokeInputMethodPicker();
+ }
+ });
+ mSetupStepGroup.addStep(step2);
+
+ final SetupStep step3 = new SetupStep(STEP_3, applicationName,
+ (TextView)findViewById(R.id.setup_step3_bullet), findViewById(R.id.setup_step3),
+ R.string.setup_step3_title, R.string.setup_step3_instruction,
+ 0 /* finishedInstruction */, R.drawable.ic_setup_step3,
+ R.string.setup_step3_action);
+ step3.setAction(new Runnable() {
+ @Override
+ public void run() {
+ invokeSubtypeEnablerOfThisIme();
+ }
+ });
+ mSetupStepGroup.addStep(step3);
+
+ mWelcomeVideoUri = new Uri.Builder()
+ .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE)
+ .authority(getPackageName())
+ .path(Integer.toString(R.raw.setup_welcome_video))
+ .build();
+ final VideoView welcomeVideoView = (VideoView)findViewById(R.id.setup_welcome_video);
+ welcomeVideoView.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
+ @Override
+ public void onPrepared(final MediaPlayer mp) {
+ // Now VideoView has been laid-out and ready to play, remove background of it to
+ // reveal the video.
+ welcomeVideoView.setBackgroundResource(0);
+ mp.setLooping(true);
+ }
+ });
+ welcomeVideoView.setOnErrorListener(new MediaPlayer.OnErrorListener() {
+ @Override
+ public boolean onError(final MediaPlayer mp, final int what, final int extra) {
+ Log.e(TAG, "Playing welcome video causes error: what=" + what + " extra=" + extra);
+ hideWelcomeVideoAndShowWelcomeImage();
+ return true;
+ }
+ });
+ mWelcomeVideoView = welcomeVideoView;
+ mWelcomeImageView = (ImageView)findViewById(R.id.setup_welcome_image);
+
+ mActionStart = findViewById(R.id.setup_start_label);
+ mActionStart.setOnClickListener(this);
+ mActionNext = findViewById(R.id.setup_next);
+ mActionNext.setOnClickListener(this);
+ mActionFinish = (TextView)findViewById(R.id.setup_finish);
+ TextViewCompatUtils.setCompoundDrawablesRelativeWithIntrinsicBounds(mActionFinish,
+ getResources().getDrawable(R.drawable.ic_setup_finish), null, null, null);
+ mActionFinish.setOnClickListener(this);
+ }
+
+ @Override
+ public void onClick(final View v) {
+ if (v == mActionFinish) {
+ finish();
+ return;
+ }
+ final int currentStep = determineSetupStepNumber();
+ final int nextStep;
+ if (v == mActionStart) {
+ nextStep = STEP_1;
+ } else if (v == mActionNext) {
+ nextStep = mStepNumber + 1;
+ } else if (v == mStep1Bullet && currentStep == STEP_2) {
+ nextStep = STEP_1;
+ } else {
+ nextStep = mStepNumber;
+ }
+ if (mStepNumber != nextStep) {
+ mStepNumber = nextStep;
+ updateSetupStepView();
+ }
+ }
+
+ void invokeSetupWizardOfThisIme() {
+ final Intent intent = new Intent();
+ intent.setClass(this, SetupWizardActivity.class);
+ intent.setFlags(Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
+ | Intent.FLAG_ACTIVITY_SINGLE_TOP
+ | Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ startActivity(intent);
+ mNeedsToAdjustStepNumberToSystemState = true;
+ }
+
+ private void invokeSettingsOfThisIme() {
+ final Intent intent = new Intent();
+ intent.setClass(this, SettingsActivity.class);
+ intent.setFlags(Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
+ | Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ startActivity(intent);
+ }
+
+ void invokeLanguageAndInputSettings() {
+ final Intent intent = new Intent();
+ intent.setAction(Settings.ACTION_INPUT_METHOD_SETTINGS);
+ intent.addCategory(Intent.CATEGORY_DEFAULT);
+ startActivity(intent);
+ mNeedsToAdjustStepNumberToSystemState = true;
+ }
+
+ void invokeInputMethodPicker() {
+ // Invoke input method picker.
+ RichInputMethodManager.getInstance().getInputMethodManager()
+ .showInputMethodPicker();
+ mNeedsToAdjustStepNumberToSystemState = true;
+ }
+
+ void invokeSubtypeEnablerOfThisIme() {
+ final InputMethodInfo imi =
+ RichInputMethodManager.getInstance().getInputMethodInfoOfThisIme();
+ final Intent intent = new Intent();
+ intent.setAction(Settings.ACTION_INPUT_METHOD_SUBTYPE_SETTINGS);
+ intent.addCategory(Intent.CATEGORY_DEFAULT);
+ intent.putExtra(Settings.EXTRA_INPUT_METHOD_ID, imi.getId());
+ startActivity(intent);
+ }
+
+ private int determineSetupStepNumberFromLauncher() {
+ final int stepNumber = determineSetupStepNumber();
+ if (stepNumber == STEP_1) {
+ return STEP_WELCOME;
+ }
+ if (stepNumber == STEP_3) {
+ return STEP_LAUNCHING_IME_SETTINGS;
+ }
+ return stepNumber;
+ }
+
+ private int determineSetupStepNumber() {
+ mHandler.cancelPollingImeSettings();
+ if (!SetupActivity.isThisImeEnabled(this)) {
+ return STEP_1;
+ }
+ if (!SetupActivity.isThisImeCurrent(this)) {
+ return STEP_2;
+ }
+ return STEP_3;
+ }
+
+ @Override
+ protected void onSaveInstanceState(final Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putInt(STATE_STEP, mStepNumber);
+ }
+
+ @Override
+ protected void onRestoreInstanceState(final Bundle savedInstanceState) {
+ super.onRestoreInstanceState(savedInstanceState);
+ mStepNumber = savedInstanceState.getInt(STATE_STEP);
+ }
+
+ private static boolean isInSetupSteps(final int stepNumber) {
+ return stepNumber >= STEP_1 && stepNumber <= STEP_3;
+ }
+
+ @Override
+ protected void onRestart() {
+ super.onRestart();
+ // Probably the setup wizard has been invoked from "Recent" menu. The setup step number
+ // needs to be adjusted to system state, because the state (IME is enabled and/or current)
+ // may have been changed.
+ if (isInSetupSteps(mStepNumber)) {
+ mStepNumber = determineSetupStepNumber();
+ }
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ if (mStepNumber == STEP_LAUNCHING_IME_SETTINGS) {
+ // Prevent white screen flashing while launching settings activity.
+ mSetupWizard.setVisibility(View.INVISIBLE);
+ invokeSettingsOfThisIme();
+ mStepNumber = STEP_BACK_FROM_IME_SETTINGS;
+ return;
+ }
+ if (mStepNumber == STEP_BACK_FROM_IME_SETTINGS) {
+ finish();
+ return;
+ }
+ updateSetupStepView();
+ }
+
+ @Override
+ public void onBackPressed() {
+ if (mStepNumber == STEP_1) {
+ mStepNumber = STEP_WELCOME;
+ updateSetupStepView();
+ return;
+ }
+ super.onBackPressed();
+ }
+
+ void hideWelcomeVideoAndShowWelcomeImage() {
+ mWelcomeVideoView.setVisibility(View.GONE);
+ mWelcomeImageView.setImageResource(R.raw.setup_welcome_image);
+ mWelcomeImageView.setVisibility(View.VISIBLE);
+ }
+
+ private void showAndStartWelcomeVideo() {
+ mWelcomeVideoView.setVisibility(View.VISIBLE);
+ mWelcomeVideoView.setVideoURI(mWelcomeVideoUri);
+ mWelcomeVideoView.start();
+ }
+
+ private void hideAndStopWelcomeVideo() {
+ mWelcomeVideoView.stopPlayback();
+ mWelcomeVideoView.setVisibility(View.GONE);
+ }
+
+ @Override
+ protected void onPause() {
+ hideAndStopWelcomeVideo();
+ super.onPause();
+ }
+
+ @Override
+ public void onWindowFocusChanged(final boolean hasFocus) {
+ super.onWindowFocusChanged(hasFocus);
+ if (hasFocus && mNeedsToAdjustStepNumberToSystemState) {
+ mNeedsToAdjustStepNumberToSystemState = false;
+ mStepNumber = determineSetupStepNumber();
+ updateSetupStepView();
+ }
+ }
+
+ private void updateSetupStepView() {
+ mSetupWizard.setVisibility(View.VISIBLE);
+ final boolean welcomeScreen = (mStepNumber == STEP_WELCOME);
+ mWelcomeScreen.setVisibility(welcomeScreen ? View.VISIBLE : View.GONE);
+ mSetupScreen.setVisibility(welcomeScreen ? View.GONE : View.VISIBLE);
+ if (welcomeScreen) {
+ if (ENABLE_WELCOME_VIDEO) {
+ showAndStartWelcomeVideo();
+ } else {
+ hideWelcomeVideoAndShowWelcomeImage();
+ }
+ return;
+ }
+ hideAndStopWelcomeVideo();
+ final boolean isStepActionAlreadyDone = mStepNumber < determineSetupStepNumber();
+ mSetupStepGroup.enableStep(mStepNumber, isStepActionAlreadyDone);
+ mActionNext.setVisibility(isStepActionAlreadyDone ? View.VISIBLE : View.GONE);
+ mActionFinish.setVisibility((mStepNumber == STEP_3) ? View.VISIBLE : View.GONE);
+ }
+
+ static final class SetupStep implements View.OnClickListener {
+ public final int mStepNo;
+ private final View mStepView;
+ private final TextView mBulletView;
+ private final int mActivatedColor;
+ private final int mDeactivatedColor;
+ private final String mInstruction;
+ private final String mFinishedInstruction;
+ private final TextView mActionLabel;
+ private Runnable mAction;
+
+ public SetupStep(final int stepNo, final String applicationName, final TextView bulletView,
+ final View stepView, final int title, final int instruction,
+ final int finishedInstruction, final int actionIcon, final int actionLabel) {
+ mStepNo = stepNo;
+ mStepView = stepView;
+ mBulletView = bulletView;
+ final Resources res = stepView.getResources();
+ mActivatedColor = res.getColor(R.color.setup_text_action);
+ mDeactivatedColor = res.getColor(R.color.setup_text_dark);
+
+ final TextView titleView = (TextView)mStepView.findViewById(R.id.setup_step_title);
+ titleView.setText(res.getString(title, applicationName));
+ mInstruction = (instruction == 0) ? null
+ : res.getString(instruction, applicationName);
+ mFinishedInstruction = (finishedInstruction == 0) ? null
+ : res.getString(finishedInstruction, applicationName);
+
+ mActionLabel = (TextView)mStepView.findViewById(R.id.setup_step_action_label);
+ mActionLabel.setText(res.getString(actionLabel));
+ if (actionIcon == 0) {
+ final int paddingEnd = ViewCompatUtils.getPaddingEnd(mActionLabel);
+ ViewCompatUtils.setPaddingRelative(mActionLabel, paddingEnd, 0, paddingEnd, 0);
+ } else {
+ TextViewCompatUtils.setCompoundDrawablesRelativeWithIntrinsicBounds(
+ mActionLabel, res.getDrawable(actionIcon), null, null, null);
+ }
+ }
+
+ public void setEnabled(final boolean enabled, final boolean isStepActionAlreadyDone) {
+ mStepView.setVisibility(enabled ? View.VISIBLE : View.GONE);
+ mBulletView.setTextColor(enabled ? mActivatedColor : mDeactivatedColor);
+ final TextView instructionView = (TextView)mStepView.findViewById(
+ R.id.setup_step_instruction);
+ instructionView.setText(isStepActionAlreadyDone ? mFinishedInstruction : mInstruction);
+ mActionLabel.setVisibility(isStepActionAlreadyDone ? View.GONE : View.VISIBLE);
+ }
+
+ public void setAction(final Runnable action) {
+ mActionLabel.setOnClickListener(this);
+ mAction = action;
+ }
+
+ @Override
+ public void onClick(final View v) {
+ if (v == mActionLabel && mAction != null) {
+ mAction.run();
+ return;
+ }
+ }
+ }
+
+ static final class SetupStepGroup {
+ private final SetupStepIndicatorView mIndicatorView;
+ private final ArrayList<SetupStep> mGroup = CollectionUtils.newArrayList();
+
+ public SetupStepGroup(final SetupStepIndicatorView indicatorView) {
+ mIndicatorView = indicatorView;
+ }
+
+ public void addStep(final SetupStep step) {
+ mGroup.add(step);
+ }
+
+ public void enableStep(final int enableStepNo, final boolean isStepActionAlreadyDone) {
+ for (final SetupStep step : mGroup) {
+ step.setEnabled(step.mStepNo == enableStepNo, isStepActionAlreadyDone);
+ }
+ mIndicatorView.setIndicatorPosition(enableStepNo - STEP_1, mGroup.size());
+ }
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java
index 166705954..13fcaf48a 100644
--- a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java
+++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java
@@ -23,7 +23,7 @@ import android.service.textservice.SpellCheckerService;
import android.util.Log;
import android.view.textservice.SuggestionsInfo;
-import com.android.inputmethod.keyboard.ProximityInfo;
+import com.android.inputmethod.keyboard.KeyboardLayoutSet;
import com.android.inputmethod.latin.BinaryDictionary;
import com.android.inputmethod.latin.CollectionUtils;
import com.android.inputmethod.latin.ContactsBinaryDictionary;
@@ -64,8 +64,6 @@ public final class AndroidSpellCheckerService extends SpellCheckerService
CollectionUtils.newSynchronizedTreeMap();
private ContactsBinaryDictionary mContactsDictionary;
- // The threshold for a candidate to be offered as a suggestion.
- private float mSuggestionThreshold;
// The threshold for a suggestion to be considered "recommended".
private float mRecommendedThreshold;
// Whether to use the contacts dictionary
@@ -112,8 +110,6 @@ public final class AndroidSpellCheckerService extends SpellCheckerService
@Override public void onCreate() {
super.onCreate();
- mSuggestionThreshold =
- Float.parseFloat(getString(R.string.spellchecker_suggestion_threshold_value));
mRecommendedThreshold =
Float.parseFloat(getString(R.string.spellchecker_recommended_threshold_value));
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
@@ -130,6 +126,19 @@ public final class AndroidSpellCheckerService extends SpellCheckerService
return script;
}
+ private static String getKeyboardLayoutNameForScript(final int script) {
+ switch (script) {
+ case AndroidSpellCheckerService.SCRIPT_LATIN:
+ return "qwerty";
+ case AndroidSpellCheckerService.SCRIPT_CYRILLIC:
+ return "east_slavic";
+ case AndroidSpellCheckerService.SCRIPT_GREEK:
+ return "greek";
+ default:
+ throw new RuntimeException("Wrong script supplied: " + script);
+ }
+ }
+
@Override
public void onSharedPreferenceChanged(final SharedPreferences prefs, final String key) {
if (!PREF_USE_CONTACTS_KEY.equals(key)) return;
@@ -198,8 +207,7 @@ public final class AndroidSpellCheckerService extends SpellCheckerService
}
public SuggestionsGatherer newSuggestionsGatherer(final String text, int maxLength) {
- return new SuggestionsGatherer(
- text, mSuggestionThreshold, mRecommendedThreshold, maxLength);
+ return new SuggestionsGatherer(text, mRecommendedThreshold, maxLength);
}
// TODO: remove this class and replace it by storage local to the session.
@@ -217,7 +225,6 @@ public final class AndroidSpellCheckerService extends SpellCheckerService
private final ArrayList<String> mSuggestions;
private final int[] mScores;
private final String mOriginalText;
- private final float mSuggestionThreshold;
private final float mRecommendedThreshold;
private final int mMaxLength;
private int mLength = 0;
@@ -227,10 +234,9 @@ public final class AndroidSpellCheckerService extends SpellCheckerService
private String mBestSuggestion = null;
private int mBestScore = Integer.MIN_VALUE; // As small as possible
- SuggestionsGatherer(final String originalText, final float suggestionThreshold,
- final float recommendedThreshold, final int maxLength) {
+ SuggestionsGatherer(final String originalText, final float recommendedThreshold,
+ final int maxLength) {
mOriginalText = originalText;
- mSuggestionThreshold = suggestionThreshold;
mRecommendedThreshold = recommendedThreshold;
mMaxLength = maxLength;
mSuggestions = CollectionUtils.newArrayList(maxLength + 1);
@@ -392,9 +398,13 @@ public final class AndroidSpellCheckerService extends SpellCheckerService
return pool;
}
- public DictAndProximity createDictAndProximity(final Locale locale) {
+ public DictAndKeyboard createDictAndKeyboard(final Locale locale) {
final int script = getScriptFromLocale(locale);
- final ProximityInfo proximityInfo = new SpellCheckerProximityInfo(script);
+ final String keyboardLayoutName = getKeyboardLayoutNameForScript(script);
+ final KeyboardLayoutSet keyboardLayoutSet =
+ KeyboardLayoutSet.createKeyboardSetForSpellChecker(this, locale.toString(),
+ keyboardLayoutName);
+
final DictionaryCollection dictionaryCollection =
DictionaryFactory.createMainDictionaryFromManager(this, locale,
true /* useFullEditDistance */);
@@ -419,6 +429,6 @@ public final class AndroidSpellCheckerService extends SpellCheckerService
mDictionaryCollectionsList.add(
new WeakReference<DictionaryCollection>(dictionaryCollection));
}
- return new DictAndProximity(dictionaryCollection, proximityInfo);
+ return new DictAndKeyboard(dictionaryCollection, keyboardLayoutSet);
}
}
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java
index da8657201..16e9fb77e 100644
--- a/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java
+++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java
@@ -257,7 +257,7 @@ public abstract class AndroidWordLevelSpellCheckerSession extends Session {
}
if (shouldFilterOut(inText, mScript)) {
- DictAndProximity dictInfo = null;
+ DictAndKeyboard dictInfo = null;
try {
dictInfo = mDictionaryPool.pollWithDefaultTimeout();
if (!DictionaryPool.isAValidDictionary(dictInfo)) {
@@ -283,32 +283,26 @@ public abstract class AndroidWordLevelSpellCheckerSession extends Session {
//suggestionsLimit);
final SuggestionsGatherer suggestionsGatherer = mService.newSuggestionsGatherer(
text, suggestionsLimit);
- final WordComposer composer = new WordComposer();
- final int length = text.length();
- for (int i = 0; i < length; i = text.offsetByCodePoints(i, 1)) {
- final int codePoint = text.codePointAt(i);
- // The getXYForCodePointAndScript method returns (Y << 16) + X
- final int xy = SpellCheckerProximityInfo.getXYForCodePointAndScript(
- codePoint, mScript);
- if (SpellCheckerProximityInfo.NOT_A_COORDINATE_PAIR == xy) {
- composer.add(codePoint,
- Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE);
- } else {
- composer.add(codePoint, xy & 0xFFFF, xy >> 16);
- }
- }
final int capitalizeType = StringUtils.getCapitalizationType(text);
boolean isInDict = true;
- DictAndProximity dictInfo = null;
+ DictAndKeyboard dictInfo = null;
try {
dictInfo = mDictionaryPool.pollWithDefaultTimeout();
if (!DictionaryPool.isAValidDictionary(dictInfo)) {
return AndroidSpellCheckerService.getNotInDictEmptySuggestions();
}
+ final WordComposer composer = new WordComposer();
+ final int length = text.length();
+ for (int i = 0; i < length; i = text.offsetByCodePoints(i, 1)) {
+ final int codePoint = text.codePointAt(i);
+ composer.addKeyInfo(codePoint, dictInfo.getKeyboard(codePoint));
+ }
+ // TODO: make a spell checker option to block offensive words or not
final ArrayList<SuggestedWordInfo> suggestions =
dictInfo.mDictionary.getSuggestions(composer, prevWord,
- dictInfo.mProximityInfo);
+ dictInfo.getProximityInfo(),
+ true /* blockOffensiveWords */);
for (final SuggestedWordInfo suggestion : suggestions) {
final String suggestionStr = suggestion.mWord;
suggestionsGatherer.addWord(suggestionStr.toCharArray(), null, 0,
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/DictAndKeyboard.java b/java/src/com/android/inputmethod/latin/spellcheck/DictAndKeyboard.java
new file mode 100644
index 000000000..b77f3e2c5
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/spellcheck/DictAndKeyboard.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2011 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.spellcheck;
+
+import com.android.inputmethod.latin.Dictionary;
+import com.android.inputmethod.keyboard.Keyboard;
+import com.android.inputmethod.keyboard.KeyboardId;
+import com.android.inputmethod.keyboard.KeyboardLayoutSet;
+import com.android.inputmethod.keyboard.ProximityInfo;
+
+/**
+ * A container for a Dictionary and a Keyboard.
+ */
+public final class DictAndKeyboard {
+ public final Dictionary mDictionary;
+ private final Keyboard mKeyboard;
+ private final Keyboard mManualShiftedKeyboard;
+
+ public DictAndKeyboard(
+ final Dictionary dictionary, final KeyboardLayoutSet keyboardLayoutSet) {
+ mDictionary = dictionary;
+ if (keyboardLayoutSet == null) {
+ mKeyboard = null;
+ mManualShiftedKeyboard = null;
+ return;
+ }
+ mKeyboard = keyboardLayoutSet.getKeyboard(KeyboardId.ELEMENT_ALPHABET);
+ mManualShiftedKeyboard =
+ keyboardLayoutSet.getKeyboard(KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED);
+ }
+
+ public Keyboard getKeyboard(final int codePoint) {
+ if (mKeyboard == null) {
+ return null;
+ }
+ return mKeyboard.getKey(codePoint) != null ? mKeyboard : mManualShiftedKeyboard;
+ }
+
+ public ProximityInfo getProximityInfo() {
+ return mKeyboard == null ? null : mKeyboard.getProximityInfo();
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/DictAndProximity.java b/java/src/com/android/inputmethod/latin/spellcheck/DictAndProximity.java
deleted file mode 100644
index 017a4f555..000000000
--- a/java/src/com/android/inputmethod/latin/spellcheck/DictAndProximity.java
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * Copyright (C) 2011 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.spellcheck;
-
-import com.android.inputmethod.latin.Dictionary;
-import com.android.inputmethod.keyboard.ProximityInfo;
-
-/**
- * A simple container for both a Dictionary and a ProximityInfo.
- */
-public final class DictAndProximity {
- public final Dictionary mDictionary;
- public final ProximityInfo mProximityInfo;
- public DictAndProximity(final Dictionary dictionary, final ProximityInfo proximityInfo) {
- mDictionary = dictionary;
- mProximityInfo = proximityInfo;
- }
-}
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/DictionaryPool.java b/java/src/com/android/inputmethod/latin/spellcheck/DictionaryPool.java
index 81dd92d9e..a20e09ee8 100644
--- a/java/src/com/android/inputmethod/latin/spellcheck/DictionaryPool.java
+++ b/java/src/com/android/inputmethod/latin/spellcheck/DictionaryPool.java
@@ -36,7 +36,7 @@ import java.util.concurrent.TimeUnit;
* the client code, but may help with sloppy clients.
*/
@SuppressWarnings("serial")
-public final class DictionaryPool extends LinkedBlockingQueue<DictAndProximity> {
+public final class DictionaryPool extends LinkedBlockingQueue<DictAndKeyboard> {
private final static String TAG = DictionaryPool.class.getSimpleName();
// How many seconds we wait for a dictionary to become available. Past this delay, we give up in
// fear some bug caused a deadlock, and reset the whole pool.
@@ -47,11 +47,12 @@ public final class DictionaryPool extends LinkedBlockingQueue<DictAndProximity>
private int mSize;
private volatile boolean mClosed;
final static ArrayList<SuggestedWordInfo> noSuggestions = CollectionUtils.newArrayList();
- private final static DictAndProximity dummyDict = new DictAndProximity(
+ private final static DictAndKeyboard dummyDict = new DictAndKeyboard(
new Dictionary(Dictionary.TYPE_MAIN) {
@Override
public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer,
- final String prevWord, final ProximityInfo proximityInfo) {
+ final String prevWord, final ProximityInfo proximityInfo,
+ final boolean blockOffensiveWords) {
return noSuggestions;
}
@Override
@@ -63,7 +64,7 @@ public final class DictionaryPool extends LinkedBlockingQueue<DictAndProximity>
}
}, null);
- static public boolean isAValidDictionary(final DictAndProximity dictInfo) {
+ static public boolean isAValidDictionary(final DictAndKeyboard dictInfo) {
return null != dictInfo && dummyDict != dictInfo;
}
@@ -78,32 +79,32 @@ public final class DictionaryPool extends LinkedBlockingQueue<DictAndProximity>
}
@Override
- public DictAndProximity poll(final long timeout, final TimeUnit unit)
+ public DictAndKeyboard poll(final long timeout, final TimeUnit unit)
throws InterruptedException {
- final DictAndProximity dict = poll();
+ final DictAndKeyboard dict = poll();
if (null != dict) return dict;
synchronized(this) {
if (mSize >= mMaxSize) {
// Our pool is already full. Wait until some dictionary is ready, or TIMEOUT
// expires to avoid a deadlock.
- final DictAndProximity result = super.poll(timeout, unit);
+ final DictAndKeyboard result = super.poll(timeout, unit);
if (null == result) {
Log.e(TAG, "Deadlock detected ! Resetting dictionary pool");
clear();
mSize = 1;
- return mService.createDictAndProximity(mLocale);
+ return mService.createDictAndKeyboard(mLocale);
} else {
return result;
}
} else {
++mSize;
- return mService.createDictAndProximity(mLocale);
+ return mService.createDictAndKeyboard(mLocale);
}
}
}
// Convenience method
- public DictAndProximity pollWithDefaultTimeout() {
+ public DictAndKeyboard pollWithDefaultTimeout() {
try {
return poll(TIMEOUT, TimeUnit.SECONDS);
} catch (InterruptedException e) {
@@ -114,7 +115,7 @@ public final class DictionaryPool extends LinkedBlockingQueue<DictAndProximity>
public void close() {
synchronized(this) {
mClosed = true;
- for (DictAndProximity dict : this) {
+ for (DictAndKeyboard dict : this) {
dict.mDictionary.close();
}
clear();
@@ -122,7 +123,7 @@ public final class DictionaryPool extends LinkedBlockingQueue<DictAndProximity>
}
@Override
- public boolean offer(final DictAndProximity dict) {
+ public boolean offer(final DictAndKeyboard dict) {
if (mClosed) {
dict.mDictionary.close();
return super.offer(dummyDict);
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerProximityInfo.java b/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerProximityInfo.java
deleted file mode 100644
index 0c480eaba..000000000
--- a/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerProximityInfo.java
+++ /dev/null
@@ -1,462 +0,0 @@
-/*
- * Copyright (C) 2011 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.spellcheck;
-
-import android.util.SparseIntArray;
-
-import com.android.inputmethod.keyboard.ProximityInfo;
-import com.android.inputmethod.latin.Constants;
-
-public final class SpellCheckerProximityInfo extends ProximityInfo {
- public SpellCheckerProximityInfo(final int script) {
- super(getProximityForScript(script), PROXIMITY_GRID_WIDTH, PROXIMITY_GRID_HEIGHT);
- }
-
- private static final int NUL = Constants.NOT_A_CODE;
-
- // This must be the same as MAX_PROXIMITY_CHARS_SIZE else it will not work inside
- // native code - this value is passed at creation of the binary object and reused
- // as the size of the passed array afterwards so they can't be different.
- private static final int ROW_SIZE = ProximityInfo.MAX_PROXIMITY_CHARS_SIZE;
-
- // The number of keys in a row of the grid used by the spell checker.
- private static final int PROXIMITY_GRID_WIDTH = 11;
- // The number of rows in the grid used by the spell checker.
- private static final int PROXIMITY_GRID_HEIGHT = 3;
-
- private static final int NOT_AN_INDEX = -1;
- public static final int NOT_A_COORDINATE_PAIR = -1;
-
- // Helper methods
- static void buildProximityIndices(final int[] proximity, final int rowSize,
- final SparseIntArray indices) {
- for (int i = 0; i < proximity.length; i += rowSize) {
- if (NUL != proximity[i]) indices.put(proximity[i], i / rowSize);
- }
- }
-
- private static final class Latin {
- // The proximity here is the union of
- // - the proximity for a QWERTY keyboard.
- // - the proximity for an AZERTY keyboard.
- // - the proximity for a QWERTZ keyboard.
- // ...plus, add all characters in the ('a', 'e', 'i', 'o', 'u') set to each other.
- //
- // The reasoning behind this construction is, almost any alphabetic text we may want
- // to spell check has been entered with one of the keyboards above. Also, specifically
- // to English, many spelling errors consist of the last vowel of the word being wrong
- // because in English vowels tend to merge with each other in pronunciation.
- /*
- The Qwerty layout this represents looks like the following:
- q w e r t y u i o p
- a s d f g h j k l
- z x c v b n m
- */
- static final int[] PROXIMITY = {
- // Proximity for row 1. This must have exactly ROW_SIZE entries for each letter,
- // and exactly PROXIMITY_GRID_WIDTH letters for a row. Pad with NUL's.
- // The number of rows must be exactly PROXIMITY_GRID_HEIGHT.
- 'q', 'w', 's', 'a', 'z', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- 'w', 'q', 'a', 's', 'd', 'e', 'x', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- 'e', 'w', 's', 'd', 'f', 'r', 'a', 'i', 'o', 'u', NUL, NUL, NUL, NUL, NUL, NUL,
- 'r', 'e', 'd', 'f', 'g', 't', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- 't', 'r', 'f', 'g', 'h', 'y', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- 'y', 't', 'g', 'h', 'j', 'u', 'a', 's', 'd', 'x', NUL, NUL, NUL, NUL, NUL, NUL,
- 'u', 'y', 'h', 'j', 'k', 'i', 'a', 'e', 'o', NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- 'i', 'u', 'j', 'k', 'l', 'o', 'a', 'e', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- 'o', 'i', 'k', 'l', 'p', 'a', 'e', 'u', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- 'p', 'o', 'l', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-
- // Proximity for row 2. See comment above about size.
- 'a', 'z', 'x', 's', 'w', 'q', 'e', 'i', 'o', 'u', NUL, NUL, NUL, NUL, NUL, NUL,
- 's', 'q', 'a', 'z', 'x', 'c', 'd', 'e', 'w', NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- 'd', 'w', 's', 'x', 'c', 'v', 'f', 'r', 'e', NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- 'f', 'e', 'd', 'c', 'v', 'b', 'g', 't', 'r', NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- 'g', 'r', 'f', 'v', 'b', 'n', 'h', 'y', 't', NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- 'h', 't', 'g', 'b', 'n', 'm', 'j', 'u', 'y', NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- 'j', 'y', 'h', 'n', 'm', 'k', 'i', 'u', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- 'k', 'u', 'j', 'm', 'l', 'o', 'i', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- 'l', 'i', 'k', 'p', 'o', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-
- // Proximity for row 3. See comment above about size.
- 'z', 'a', 's', 'd', 'x', 't', 'g', 'h', 'j', 'u', 'q', 'e', NUL, NUL, NUL, NUL,
- 'x', 'z', 'a', 's', 'd', 'c', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- 'c', 'x', 's', 'd', 'f', 'v', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- 'v', 'c', 'd', 'f', 'g', 'b', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- 'b', 'v', 'f', 'g', 'h', 'n', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- 'n', 'b', 'g', 'h', 'j', 'm', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- 'm', 'n', 'h', 'j', 'k', 'l', 'o', 'p', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- };
-
- // This is a mapping array from the code point to the index in the PROXIMITY array.
- // When we check the spelling of a word, we need to pass (x,y) coordinates to the native
- // code for each letter of the word. These are most easily computed from the index in the
- // PROXIMITY array. Since we'll need to do that very often, the index lookup from the code
- // point needs to be as fast as possible, and a map is probably the best way to do this.
- // To avoid unnecessary boxing conversion to Integer, here we use SparseIntArray.
- static final SparseIntArray INDICES = new SparseIntArray(PROXIMITY.length / ROW_SIZE);
-
- static {
- buildProximityIndices(PROXIMITY, ROW_SIZE, INDICES);
- }
- }
-
- private static final class Cyrillic {
- // TODO: The following table is solely based on the keyboard layout. Consult with Russian
- // speakers on commonly misspelled words/letters.
- /*
- The Russian layout this represents looks like the following:
- й ц у к е н г ш щ з х
- ф ы в а п р о л д ж э
- я ч с м и т ь б ю
-
- This gives us the following table:
- 'й', 'ц', 'ф', 'ы', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- 'ц', 'й', 'ф', 'ы', 'в', 'у', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- 'у', 'ц', 'ы', 'в', 'а', 'к', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- 'к', 'у', 'в', 'а', 'п', 'е', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- 'е', 'к', 'а', 'п', 'р', 'н', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- 'н', 'е', 'п', 'р', 'о', 'г', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- 'г', 'н', 'р', 'о', 'л', 'ш', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- 'ш', 'г', 'о', 'л', 'д', 'щ', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- 'щ', 'ш', 'л', 'д', 'ж', 'з', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- 'з', 'щ', 'д', 'ж', 'э', 'х', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- 'х', 'з', 'ж', 'э', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-
- 'ф', 'й', 'ц', 'ы', 'я', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- 'ы', 'й', 'ц', 'у', 'ф', 'в', 'я', 'ч', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- 'в', 'ц', 'у', 'к', 'ы', 'а', 'я', 'ч', 'с', NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- 'а', 'у', 'к', 'е', 'в', 'п', 'ч', 'с', 'м', NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- 'п', 'к', 'е', 'н', 'а', 'р', 'с', 'м', 'и', NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- 'р', 'е', 'н', 'г', 'п', 'о', 'м', 'и', 'т', NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- 'о', 'н', 'г', 'ш', 'р', 'л', 'и', 'т', 'ь', NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- 'л', 'г', 'ш', 'щ', 'о', 'д', 'т', 'ь', 'б', NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- 'д', 'ш', 'щ', 'з', 'л', 'ж', 'ь', 'б', 'ю', NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- 'ж', 'щ', 'з', 'х', 'д', 'э', 'б', 'ю', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- 'э', 'з', 'х', 'ю', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-
- 'я', 'ф', 'ы', 'в', 'ч', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- 'ч', 'ы', 'в', 'а', 'я', 'с', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- 'с', 'в', 'а', 'п', 'ч', 'м', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- 'м', 'а', 'п', 'р', 'с', 'и', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- 'и', 'п', 'р', 'о', 'м', 'т', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- 'т', 'р', 'о', 'л', 'и', 'ь', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- 'ь', 'о', 'л', 'д', 'т', 'б', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- 'б', 'л', 'д', 'ж', 'ь', 'ю', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- 'ю', 'д', 'ж', 'э', 'б', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-
- Using the following characters:
- */
- private static final int CY_SHORT_I = '\u0439'; // й
- private static final int CY_TSE = '\u0446'; // ц
- private static final int CY_U = '\u0443'; // у
- private static final int CY_KA = '\u043A'; // к
- private static final int CY_IE = '\u0435'; // е
- private static final int CY_EN = '\u043D'; // н
- private static final int CY_GHE = '\u0433'; // г
- private static final int CY_SHA = '\u0448'; // ш
- private static final int CY_SHCHA = '\u0449'; // щ
- private static final int CY_ZE = '\u0437'; // з
- private static final int CY_HA = '\u0445'; // х
- private static final int CY_EF = '\u0444'; // ф
- private static final int CY_YERU = '\u044B'; // ы
- private static final int CY_VE = '\u0432'; // в
- private static final int CY_A = '\u0430'; // а
- private static final int CY_PE = '\u043F'; // п
- private static final int CY_ER = '\u0440'; // р
- private static final int CY_O = '\u043E'; // о
- private static final int CY_EL = '\u043B'; // л
- private static final int CY_DE = '\u0434'; // д
- private static final int CY_ZHE = '\u0436'; // ж
- private static final int CY_E = '\u044D'; // э
- private static final int CY_YA = '\u044F'; // я
- private static final int CY_CHE = '\u0447'; // ч
- private static final int CY_ES = '\u0441'; // с
- private static final int CY_EM = '\u043C'; // м
- private static final int CY_I = '\u0438'; // и
- private static final int CY_TE = '\u0442'; // т
- private static final int CY_SOFT_SIGN = '\u044C'; // ь
- private static final int CY_BE = '\u0431'; // б
- private static final int CY_YU = '\u044E'; // ю
- static final int[] PROXIMITY = {
- // Proximity for row 1. This must have exactly ROW_SIZE entries for each letter,
- // and exactly PROXIMITY_GRID_WIDTH letters for a row. Pad with NUL's.
- // The number of rows must be exactly PROXIMITY_GRID_HEIGHT.
- CY_SHORT_I, CY_TSE, CY_EF, CY_YERU, NUL, NUL, NUL, NUL,
- NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- CY_TSE, CY_SHORT_I, CY_EF, CY_YERU, CY_VE, CY_U, NUL, NUL,
- NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- CY_U, CY_TSE, CY_YERU, CY_VE, CY_A, CY_KA, NUL, NUL,
- NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- CY_KA, CY_U, CY_VE, CY_A, CY_PE, CY_IE, NUL, NUL,
- NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- CY_IE, CY_KA, CY_A, CY_PE, CY_ER, CY_EN, NUL, NUL,
- NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- CY_EN, CY_IE, CY_PE, CY_ER, CY_O, CY_GHE, NUL, NUL,
- NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- CY_GHE, CY_EN, CY_ER, CY_O, CY_EL, CY_SHA, NUL, NUL,
- NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- CY_SHA, CY_GHE, CY_O, CY_EL, CY_DE, CY_SHCHA, NUL, NUL,
- NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- CY_SHCHA, CY_SHA, CY_EL, CY_DE, CY_ZHE, CY_ZE, NUL, NUL,
- NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- CY_ZE, CY_SHCHA, CY_DE, CY_ZHE, CY_E, CY_HA, NUL, NUL,
- NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- CY_HA, CY_ZE, CY_ZHE, CY_E, NUL, NUL, NUL, NUL,
- NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-
- // Proximity for row 2. See comment above about size.
- CY_EF, CY_SHORT_I, CY_TSE, CY_YERU, CY_YA, NUL, NUL, NUL,
- NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- CY_YERU, CY_SHORT_I, CY_TSE, CY_U, CY_EF, CY_VE, CY_YA, CY_CHE,
- NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- CY_VE, CY_TSE, CY_U, CY_KA, CY_YERU, CY_A, CY_YA, CY_CHE,
- CY_ES, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- CY_A, CY_U, CY_KA, CY_IE, CY_VE, CY_PE, CY_CHE, CY_ES,
- CY_EM, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- CY_PE, CY_KA, CY_IE, CY_EN, CY_A, CY_ER, CY_ES, CY_EM,
- CY_I, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- CY_ER, CY_IE, CY_EN, CY_GHE, CY_PE, CY_O, CY_EM, CY_I,
- CY_TE, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- CY_O, CY_EN, CY_GHE, CY_SHA, CY_ER, CY_EL, CY_I, CY_TE,
- CY_SOFT_SIGN, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- CY_EL, CY_GHE, CY_SHA, CY_SHCHA, CY_O, CY_DE, CY_TE, CY_SOFT_SIGN,
- CY_BE, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- CY_DE, CY_SHA, CY_SHCHA, CY_ZE, CY_EL, CY_ZHE, CY_SOFT_SIGN, CY_BE,
- CY_YU, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- CY_ZHE, CY_SHCHA, CY_ZE, CY_HA, CY_DE, CY_E, CY_BE, CY_YU,
- NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- CY_E, CY_ZE, CY_HA, CY_YU, NUL, NUL, NUL, NUL,
- NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-
- // Proximity for row 3. See comment above about size.
- CY_YA, CY_EF, CY_YERU, CY_VE, CY_CHE, NUL, NUL, NUL,
- NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- CY_CHE, CY_YERU, CY_VE, CY_A, CY_YA, CY_ES, NUL, NUL,
- NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- CY_ES, CY_VE, CY_A, CY_PE, CY_CHE, CY_EM, NUL, NUL,
- NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- CY_EM, CY_A, CY_PE, CY_ER, CY_ES, CY_I, NUL, NUL,
- NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- CY_I, CY_PE, CY_ER, CY_O, CY_EM, CY_TE, NUL, NUL,
- NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- CY_TE, CY_ER, CY_O, CY_EL, CY_I, CY_SOFT_SIGN, NUL, NUL,
- NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- CY_SOFT_SIGN, CY_O, CY_EL, CY_DE, CY_TE, CY_BE, NUL, NUL,
- NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- CY_BE, CY_EL, CY_DE, CY_ZHE, CY_SOFT_SIGN, CY_YU, NUL, NUL,
- NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- CY_YU, CY_DE, CY_ZHE, CY_E, CY_BE, NUL, NUL, NUL,
- NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- };
-
- static final SparseIntArray INDICES = new SparseIntArray(PROXIMITY.length / ROW_SIZE);
-
- static {
- buildProximityIndices(PROXIMITY, ROW_SIZE, INDICES);
- }
- }
-
- private static final class Greek {
- // TODO: The following table is solely based on the keyboard layout. Consult with Greek
- // speakers on commonly misspelled words/letters.
- /*
- The Greek layout this represents looks like the following:
- ; ς ε ρ τ υ θ ι ο π
- α σ δ φ γ η ξ κ λ
- ζ χ ψ ω β ν μ
-
- This gives us the following table:
- 'ς', 'ε', 'α', 'σ', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- 'ε', 'ς', 'ρ', 'σ', 'δ', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- 'ρ', 'ε', 'τ', 'δ', 'φ', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- 'τ', 'ρ', 'υ', 'φ', 'γ', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- 'υ', 'τ', 'θ', 'γ', 'η', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- 'θ', 'υ', 'ι', 'η', 'ξ', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- 'ι', 'θ', 'ο', 'ξ', 'κ', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- 'ο', 'ι', 'π', 'κ', 'λ', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- 'π', 'ο', 'λ', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-
- 'α', 'ς', 'σ', 'ζ', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- 'σ', 'ς', 'ε', 'α', 'δ', 'ζ', 'χ', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- 'δ', 'ε', 'ρ', 'σ', 'φ', 'ζ', 'χ', 'ψ', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- 'φ', 'ρ', 'τ', 'δ', 'γ', 'χ', 'ψ', 'ω', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- 'γ', 'τ', 'υ', 'φ', 'η', 'ψ', 'ω', 'β', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- 'η', 'υ', 'θ', 'γ', 'ξ', 'ω', 'β', 'ν', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- 'ξ', 'θ', 'ι', 'η', 'κ', 'β', 'ν', 'μ', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- 'κ', 'ι', 'ο', 'ξ', 'λ', 'ν', 'μ', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- 'λ', 'ο', 'π', 'κ', 'μ', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-
- 'ζ', 'α', 'σ', 'δ', 'χ', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- 'χ', 'σ', 'δ', 'φ', 'ζ', 'ψ', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- 'ψ', 'δ', 'φ', 'γ', 'χ', 'ω', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- 'ω', 'φ', 'γ', 'η', 'ψ', 'β', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- 'β', 'γ', 'η', 'ξ', 'ω', 'ν', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- 'ν', 'η', 'ξ', 'κ', 'β', 'μ', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- 'μ', 'ξ', 'κ', 'λ', 'ν', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-
- Using the following characters:
- */
- private static final int GR_FINAL_SIGMA = '\u03C2'; // ς
- private static final int GR_EPSILON = '\u03B5'; // ε
- private static final int GR_RHO = '\u03C1'; // ρ
- private static final int GR_TAU = '\u03C4'; // τ
- private static final int GR_UPSILON = '\u03C5'; // υ
- private static final int GR_THETA = '\u03B8'; // θ
- private static final int GR_IOTA = '\u03B9'; // ι
- private static final int GR_OMICRON = '\u03BF'; // ο
- private static final int GR_PI = '\u03C0'; // π
- private static final int GR_ALPHA = '\u03B1'; // α
- private static final int GR_SIGMA = '\u03C3'; // σ
- private static final int GR_DELTA = '\u03B4'; // δ
- private static final int GR_PHI = '\u03C6'; // φ
- private static final int GR_GAMMA = '\u03B3'; // γ
- private static final int GR_ETA = '\u03B7'; // η
- private static final int GR_XI = '\u03BE'; // ξ
- private static final int GR_KAPPA = '\u03BA'; // κ
- private static final int GR_LAMDA = '\u03BB'; // λ
- private static final int GR_ZETA = '\u03B6'; // ζ
- private static final int GR_CHI = '\u03C7'; // χ
- private static final int GR_PSI = '\u03C8'; // ψ
- private static final int GR_OMEGA = '\u03C9'; // ω
- private static final int GR_BETA = '\u03B2'; // β
- private static final int GR_NU = '\u03BD'; // ν
- private static final int GR_MU = '\u03BC'; // μ
- static final int[] PROXIMITY = {
- // Proximity for row 1. This must have exactly ROW_SIZE entries for each letter,
- // and exactly PROXIMITY_GRID_WIDTH letters for a row. Pad with NUL's.
- // The number of rows must be exactly PROXIMITY_GRID_HEIGHT.
- GR_FINAL_SIGMA, GR_EPSILON, GR_ALPHA, GR_SIGMA, NUL, NUL, NUL,
- NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- GR_EPSILON, GR_FINAL_SIGMA, GR_RHO, GR_SIGMA, GR_DELTA, NUL, NUL, NUL,
- NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- GR_RHO, GR_EPSILON, GR_TAU, GR_DELTA, GR_PHI, NUL, NUL, NUL,
- NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- GR_TAU, GR_RHO, GR_UPSILON, GR_PHI, GR_GAMMA, NUL, NUL, NUL,
- NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- GR_UPSILON, GR_TAU, GR_THETA, GR_GAMMA, GR_ETA, NUL, NUL, NUL,
- NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- GR_THETA, GR_UPSILON, GR_IOTA, GR_ETA, GR_XI, NUL, NUL, NUL,
- NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- GR_IOTA, GR_THETA, GR_OMICRON, GR_XI, GR_KAPPA, NUL, NUL, NUL,
- NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- GR_OMICRON, GR_IOTA, GR_PI, GR_KAPPA, GR_LAMDA, NUL, NUL, NUL, NUL, NUL,
- NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- GR_PI, GR_OMICRON, GR_LAMDA, NUL, NUL, NUL, NUL, NUL,
- NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-
- GR_ALPHA, GR_FINAL_SIGMA, GR_SIGMA, GR_ZETA, NUL, NUL, NUL, NUL,
- NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- GR_SIGMA, GR_FINAL_SIGMA, GR_EPSILON, GR_ALPHA, GR_DELTA, GR_ZETA, GR_CHI, NUL,
- NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- GR_DELTA, GR_EPSILON, GR_RHO, GR_SIGMA, GR_PHI, GR_ZETA, GR_CHI, GR_PSI,
- NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- GR_PHI, GR_RHO, GR_TAU, GR_DELTA, GR_GAMMA, GR_CHI, GR_PSI, GR_OMEGA,
- NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- GR_GAMMA, GR_TAU, GR_UPSILON, GR_PHI, GR_ETA, GR_PSI, GR_OMEGA, GR_BETA,
- NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- GR_ETA, GR_UPSILON, GR_THETA, GR_GAMMA, GR_XI, GR_OMEGA, GR_BETA, GR_NU,
- NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- GR_XI, GR_THETA, GR_IOTA, GR_ETA, GR_KAPPA, GR_BETA, GR_NU, GR_MU,
- NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- GR_KAPPA, GR_IOTA, GR_OMICRON, GR_XI, GR_LAMDA, GR_NU, GR_MU, NUL, NUL,
- NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- GR_LAMDA, GR_OMICRON, GR_PI, GR_KAPPA, GR_MU, NUL, NUL, NUL,
- NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-
- GR_ZETA, GR_ALPHA, GR_SIGMA, GR_DELTA, GR_CHI, NUL, NUL, NUL,
- NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- GR_CHI, GR_SIGMA, GR_DELTA, GR_PHI, GR_ZETA, GR_PSI, NUL, NUL,
- NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- GR_PSI, GR_DELTA, GR_PHI, GR_GAMMA, GR_CHI, GR_OMEGA, NUL, NUL,
- NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- GR_OMEGA, GR_PHI, GR_GAMMA, GR_ETA, GR_PSI, GR_BETA, NUL, NUL,
- NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- GR_BETA, GR_GAMMA, GR_ETA, GR_XI, GR_OMEGA, GR_NU, NUL, NUL,
- NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- GR_NU, GR_ETA, GR_XI, GR_KAPPA, GR_BETA, GR_MU, NUL, NUL,
- NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- GR_MU, GR_XI, GR_KAPPA, GR_LAMDA, GR_NU, NUL, NUL, NUL,
- NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
- };
-
- static final SparseIntArray INDICES = new SparseIntArray(PROXIMITY.length / ROW_SIZE);
-
- static {
- buildProximityIndices(PROXIMITY, ROW_SIZE, INDICES);
- }
- }
-
- private static int[] getProximityForScript(final int script) {
- switch (script) {
- case AndroidSpellCheckerService.SCRIPT_LATIN:
- return Latin.PROXIMITY;
- case AndroidSpellCheckerService.SCRIPT_CYRILLIC:
- return Cyrillic.PROXIMITY;
- case AndroidSpellCheckerService.SCRIPT_GREEK:
- return Greek.PROXIMITY;
- default:
- throw new RuntimeException("Wrong script supplied: " + script);
- }
- }
-
- private static int getIndexOfCodeForScript(final int codePoint, final int script) {
- switch (script) {
- case AndroidSpellCheckerService.SCRIPT_LATIN:
- return Latin.INDICES.get(codePoint, NOT_AN_INDEX);
- case AndroidSpellCheckerService.SCRIPT_CYRILLIC:
- return Cyrillic.INDICES.get(codePoint, NOT_AN_INDEX);
- case AndroidSpellCheckerService.SCRIPT_GREEK:
- return Greek.INDICES.get(codePoint, NOT_AN_INDEX);
- default:
- throw new RuntimeException("Wrong script supplied: " + script);
- }
- }
-
- // Returns (Y << 16) + X to avoid creating a temporary object. This is okay because
- // X and Y are limited to PROXIMITY_GRID_WIDTH resp. PROXIMITY_GRID_HEIGHT which is very
- // inferior to 1 << 16
- // As an exception, this returns NOT_A_COORDINATE_PAIR if the key is not on the grid
- public static int getXYForCodePointAndScript(final int codePoint, final int script) {
- final int index = getIndexOfCodeForScript(codePoint, script);
- if (NOT_AN_INDEX == index) return NOT_A_COORDINATE_PAIR;
- final int y = index / PROXIMITY_GRID_WIDTH;
- final int x = index % PROXIMITY_GRID_WIDTH;
- if (y > PROXIMITY_GRID_HEIGHT) {
- // Safety check, should be entirely useless
- throw new RuntimeException("Wrong y coordinate in spell checker proximity");
- }
- return (y << 16) + x;
- }
-}
diff --git a/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestions.java b/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestions.java
index 3037669c0..09f81d4c7 100644
--- a/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestions.java
+++ b/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestions.java
@@ -23,19 +23,28 @@ import android.graphics.drawable.Drawable;
import com.android.inputmethod.keyboard.Key;
import com.android.inputmethod.keyboard.Keyboard;
+import com.android.inputmethod.keyboard.KeyboardActionListener;
import com.android.inputmethod.keyboard.TypefaceUtils;
import com.android.inputmethod.keyboard.internal.KeyboardBuilder;
import com.android.inputmethod.keyboard.internal.KeyboardIconsSet;
import com.android.inputmethod.keyboard.internal.KeyboardParams;
import com.android.inputmethod.latin.R;
import com.android.inputmethod.latin.SuggestedWords;
+import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
import com.android.inputmethod.latin.Utils;
public final class MoreSuggestions extends Keyboard {
public static final int SUGGESTION_CODE_BASE = 1024;
- MoreSuggestions(final MoreSuggestionsParam params) {
+ public final SuggestedWords mSuggestedWords;
+
+ public static abstract class MoreSuggestionsListener extends KeyboardActionListener.Adapter {
+ public abstract void onSuggestionSelected(final int index, final SuggestedWordInfo info);
+ }
+
+ MoreSuggestions(final MoreSuggestionsParam params, final SuggestedWords suggestedWords) {
super(params);
+ mSuggestedWords = suggestedWords;
}
private static final class MoreSuggestionsParam extends KeyboardParams {
@@ -52,8 +61,9 @@ public final class MoreSuggestions extends Keyboard {
super();
}
- public int layout(final SuggestedWords suggestions, final int fromPos, final int maxWidth,
- final int minWidth, final int maxRow, final Paint paint, final Resources res) {
+ public int layout(final SuggestedWords suggestedWords, final int fromPos,
+ final int maxWidth, final int minWidth, final int maxRow, final Paint paint,
+ final Resources res) {
clearKeys();
mDivider = res.getDrawable(R.drawable.more_suggestions_divider);
mDividerWidth = mDivider.getIntrinsicWidth();
@@ -61,9 +71,9 @@ public final class MoreSuggestions extends Keyboard {
int row = 0;
int pos = fromPos, rowStartPos = fromPos;
- final int size = Math.min(suggestions.size(), SuggestionStripView.MAX_SUGGESTIONS);
+ final int size = Math.min(suggestedWords.size(), SuggestionStripView.MAX_SUGGESTIONS);
while (pos < size) {
- final String word = suggestions.getWord(pos);
+ final String word = suggestedWords.getWord(pos);
// TODO: Should take care of text x-scaling.
mWidths[pos] = (int)(TypefaceUtils.getLabelWidth(word, paint) + padding);
final int numColumn = pos - rowStartPos + 1;
@@ -163,7 +173,7 @@ public final class MoreSuggestions extends Keyboard {
public static final class Builder extends KeyboardBuilder<MoreSuggestionsParam> {
private final MoreSuggestionsView mPaneView;
- private SuggestedWords mSuggestions;
+ private SuggestedWords mSuggestedWords;
private int mFromPos;
private int mToPos;
@@ -172,7 +182,7 @@ public final class MoreSuggestions extends Keyboard {
mPaneView = paneView;
}
- public Builder layout(final SuggestedWords suggestions, final int fromPos,
+ public Builder layout(final SuggestedWords suggestedWords, final int fromPos,
final int maxWidth, final int minWidth, final int maxRow,
final Keyboard parentKeyboard) {
final int xmlId = R.xml.kbd_suggestions_pane_template;
@@ -180,11 +190,11 @@ public final class MoreSuggestions extends Keyboard {
mParams.mVerticalGap = mParams.mTopPadding = parentKeyboard.mVerticalGap / 2;
mPaneView.updateKeyboardGeometry(mParams.mDefaultRowHeight);
- final int count = mParams.layout(suggestions, fromPos, maxWidth, minWidth, maxRow,
+ final int count = mParams.layout(suggestedWords, fromPos, maxWidth, minWidth, maxRow,
mPaneView.newLabelPaint(null /* key */), mResources);
mFromPos = fromPos;
mToPos = fromPos + count;
- mSuggestions = suggestions;
+ mSuggestedWords = suggestedWords;
return this;
}
@@ -195,8 +205,8 @@ public final class MoreSuggestions extends Keyboard {
final int x = params.getX(pos);
final int y = params.getY(pos);
final int width = params.getWidth(pos);
- final String word = mSuggestions.getWord(pos).toString();
- final String info = Utils.getDebugInfo(mSuggestions, pos);
+ final String word = mSuggestedWords.getWord(pos);
+ final String info = Utils.getDebugInfo(mSuggestedWords, pos);
final int index = pos + SUGGESTION_CODE_BASE;
final Key key = new Key(
params, word, info, KeyboardIconsSet.ICON_UNDEFINED, index, null, x, y,
@@ -211,7 +221,7 @@ public final class MoreSuggestions extends Keyboard {
params.onAddKey(divider);
}
}
- return new MoreSuggestions(params);
+ return new MoreSuggestions(params, mSuggestedWords);
}
}
diff --git a/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestionsView.java b/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestionsView.java
index 6509f394b..d585b5c7f 100644
--- a/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestionsView.java
+++ b/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestionsView.java
@@ -18,15 +18,21 @@ package com.android.inputmethod.latin.suggestions;
import android.content.Context;
import android.util.AttributeSet;
+import android.util.Log;
+import com.android.inputmethod.keyboard.Keyboard;
import com.android.inputmethod.keyboard.MoreKeysKeyboardView;
import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.SuggestedWords;
+import com.android.inputmethod.latin.suggestions.MoreSuggestions.MoreSuggestionsListener;
/**
* A view that renders a virtual {@link MoreSuggestions}. It handles rendering of keys and detecting
* key presses and touch movements.
*/
public final class MoreSuggestionsView extends MoreKeysKeyboardView {
+ private static final String TAG = MoreSuggestionsView.class.getSimpleName();
+
public MoreSuggestionsView(final Context context, final AttributeSet attrs) {
this(context, attrs, R.attr.moreSuggestionsViewStyle);
}
@@ -54,9 +60,24 @@ public final class MoreSuggestionsView extends MoreKeysKeyboardView {
@Override
public void onCodeInput(final int code, final int x, final int y) {
+ final Keyboard keyboard = getKeyboard();
+ if (!(keyboard instanceof MoreSuggestions)) {
+ Log.e(TAG, "Expected keyboard is MoreSuggestions, but found "
+ + keyboard.getClass().getName());
+ return;
+ }
+ final SuggestedWords suggestedWords = ((MoreSuggestions)keyboard).mSuggestedWords;
final int index = code - MoreSuggestions.SUGGESTION_CODE_BASE;
- if (index >= 0 && index < SuggestionStripView.MAX_SUGGESTIONS) {
- mListener.onCustomRequest(index);
+ if (index < 0 || index >= suggestedWords.size()) {
+ Log.e(TAG, "Selected suggestion has an illegal index: " + index);
+ return;
+ }
+ if (!(mListener instanceof MoreSuggestionsListener)) {
+ Log.e(TAG, "Expected mListener is MoreSuggestionsListener, but found "
+ + mListener.getClass().getName());
+ return;
}
+ ((MoreSuggestionsListener)mListener).onSuggestionSelected(
+ index, suggestedWords.getInfo(index));
}
}
diff --git a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java
index 2a21ec2f5..ad350a02f 100644
--- a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java
+++ b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java
@@ -50,7 +50,6 @@ import android.widget.RelativeLayout;
import android.widget.TextView;
import com.android.inputmethod.keyboard.Keyboard;
-import com.android.inputmethod.keyboard.KeyboardActionListener;
import com.android.inputmethod.keyboard.KeyboardSwitcher;
import com.android.inputmethod.keyboard.MainKeyboardView;
import com.android.inputmethod.keyboard.MoreKeysPanel;
@@ -65,6 +64,7 @@ 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;
import java.util.ArrayList;
@@ -93,7 +93,7 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick
private final ArrayList<View> mDividers = CollectionUtils.newArrayList();
Listener mListener;
- SuggestedWords mSuggestedWords = SuggestedWords.EMPTY;
+ private SuggestedWords mSuggestedWords = SuggestedWords.EMPTY;
private final SuggestionStripViewParams mParams;
private static final float MIN_TEXT_XSCALE = 0.70f;
@@ -652,15 +652,11 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick
dismissMoreSuggestions();
}
- private final KeyboardActionListener mMoreSuggestionsListener =
- new KeyboardActionListener.Adapter() {
+ private final MoreSuggestionsListener mMoreSuggestionsListener = new MoreSuggestionsListener() {
@Override
- public boolean onCustomRequest(final int requestCode) {
- final int index = requestCode;
- final SuggestedWordInfo wordInfo = mSuggestedWords.getInfo(index);
+ public void onSuggestionSelected(final int index, final SuggestedWordInfo wordInfo) {
mListener.pickSuggestionManually(index, wordInfo);
dismissMoreSuggestions();
- return true;
}
@Override
diff --git a/java/src/com/android/inputmethod/latin/userdictionary/UserDictionaryAddWordContents.java b/java/src/com/android/inputmethod/latin/userdictionary/UserDictionaryAddWordContents.java
new file mode 100644
index 000000000..2b6fda381
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/userdictionary/UserDictionaryAddWordContents.java
@@ -0,0 +1,261 @@
+/*
+ * 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.userdictionary;
+
+import com.android.inputmethod.compat.UserDictionaryCompatUtils;
+import com.android.inputmethod.latin.LocaleUtils;
+import com.android.inputmethod.latin.R;
+
+import android.app.Activity;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.provider.UserDictionary;
+import android.text.TextUtils;
+import android.view.View;
+import android.widget.EditText;
+
+import java.util.ArrayList;
+import java.util.Locale;
+import java.util.TreeSet;
+
+// Caveat: This class is basically taken from
+// packages/apps/Settings/src/com/android/settings/inputmethod/UserDictionaryAddWordContents.java
+// in order to deal with some devices that have issues with the user dictionary handling
+
+/**
+ * A container class to factor common code to UserDictionaryAddWordFragment
+ * and UserDictionaryAddWordActivity.
+ */
+public class UserDictionaryAddWordContents {
+ public static final String EXTRA_MODE = "mode";
+ public static final String EXTRA_WORD = "word";
+ public static final String EXTRA_SHORTCUT = "shortcut";
+ public static final String EXTRA_LOCALE = "locale";
+ public static final String EXTRA_ORIGINAL_WORD = "originalWord";
+ public static final String EXTRA_ORIGINAL_SHORTCUT = "originalShortcut";
+
+ public static final int MODE_EDIT = 0;
+ public static final int MODE_INSERT = 1;
+
+ /* package */ static final int CODE_WORD_ADDED = 0;
+ /* package */ static final int CODE_CANCEL = 1;
+ /* package */ static final int CODE_ALREADY_PRESENT = 2;
+
+ private static final int FREQUENCY_FOR_USER_DICTIONARY_ADDS = 250;
+
+ private final int mMode; // Either MODE_EDIT or MODE_INSERT
+ private final EditText mWordEditText;
+ private final EditText mShortcutEditText;
+ private String mLocale;
+ private final String mOldWord;
+ private final String mOldShortcut;
+
+ /* package */ UserDictionaryAddWordContents(final View view, final Bundle args) {
+ mWordEditText = (EditText)view.findViewById(R.id.user_dictionary_add_word_text);
+ mShortcutEditText = (EditText)view.findViewById(R.id.user_dictionary_add_shortcut);
+ if (!UserDictionarySettings.IS_SHORTCUT_API_SUPPORTED) {
+ mShortcutEditText.setVisibility(View.GONE);
+ view.findViewById(R.id.user_dictionary_add_shortcut_label).setVisibility(View.GONE);
+ }
+ final String word = args.getString(EXTRA_WORD);
+ if (null != word) {
+ mWordEditText.setText(word);
+ mWordEditText.setSelection(word.length());
+ }
+ final String shortcut;
+ if (UserDictionarySettings.IS_SHORTCUT_API_SUPPORTED) {
+ shortcut = args.getString(EXTRA_SHORTCUT);
+ if (null != shortcut && null != mShortcutEditText) {
+ mShortcutEditText.setText(shortcut);
+ }
+ mOldShortcut = args.getString(EXTRA_SHORTCUT);
+ } else {
+ shortcut = null;
+ mOldShortcut = null;
+ }
+ mMode = args.getInt(EXTRA_MODE); // default return value for #getInt() is 0 = MODE_EDIT
+ mOldWord = args.getString(EXTRA_WORD);
+ updateLocale(args.getString(EXTRA_LOCALE));
+ }
+
+ // locale may be null, this means default locale
+ // It may also be the empty string, which means "all locales"
+ /* package */ void updateLocale(final String locale) {
+ mLocale = null == locale ? Locale.getDefault().toString() : locale;
+ }
+
+ /* package */ void saveStateIntoBundle(final Bundle outState) {
+ outState.putString(EXTRA_WORD, mWordEditText.getText().toString());
+ outState.putString(EXTRA_ORIGINAL_WORD, mOldWord);
+ if (null != mShortcutEditText) {
+ outState.putString(EXTRA_SHORTCUT, mShortcutEditText.getText().toString());
+ }
+ if (null != mOldShortcut) {
+ outState.putString(EXTRA_ORIGINAL_SHORTCUT, mOldShortcut);
+ }
+ outState.putString(EXTRA_LOCALE, mLocale);
+ }
+
+ /* package */ void delete(final Context context) {
+ if (MODE_EDIT == mMode && !TextUtils.isEmpty(mOldWord)) {
+ // Mode edit: remove the old entry.
+ final ContentResolver resolver = context.getContentResolver();
+ UserDictionarySettings.deleteWord(mOldWord, mOldShortcut, resolver);
+ }
+ // If we are in add mode, nothing was added, so we don't need to do anything.
+ }
+
+ /* package */
+ int apply(final Context context, final Bundle outParameters) {
+ if (null != outParameters) saveStateIntoBundle(outParameters);
+ final ContentResolver resolver = context.getContentResolver();
+ if (MODE_EDIT == mMode && !TextUtils.isEmpty(mOldWord)) {
+ // Mode edit: remove the old entry.
+ UserDictionarySettings.deleteWord(mOldWord, mOldShortcut, resolver);
+ }
+ final String newWord = mWordEditText.getText().toString();
+ final String newShortcut;
+ if (!UserDictionarySettings.IS_SHORTCUT_API_SUPPORTED) {
+ newShortcut = null;
+ } else if (null == mShortcutEditText) {
+ newShortcut = null;
+ } else {
+ final String tmpShortcut = mShortcutEditText.getText().toString();
+ if (TextUtils.isEmpty(tmpShortcut)) {
+ newShortcut = null;
+ } else {
+ newShortcut = tmpShortcut;
+ }
+ }
+ if (TextUtils.isEmpty(newWord)) {
+ // If the word is somehow empty, don't insert it.
+ return CODE_CANCEL;
+ }
+ // If there is no shortcut, and the word already exists in the database, then we
+ // should not insert, because either A. the word exists with no shortcut, in which
+ // case the exact same thing we want to insert is already there, or B. the word
+ // exists with at least one shortcut, in which case it has priority on our word.
+ if (hasWord(newWord, context)) return CODE_ALREADY_PRESENT;
+
+ // Disallow duplicates. If the same word with no shortcut is defined, remove it; if
+ // the same word with the same shortcut is defined, remove it; but we don't mind if
+ // there is the same word with a different, non-empty shortcut.
+ UserDictionarySettings.deleteWord(newWord, null, resolver);
+ if (!TextUtils.isEmpty(newShortcut)) {
+ // If newShortcut is empty we just deleted this, no need to do it again
+ UserDictionarySettings.deleteWord(newWord, newShortcut, resolver);
+ }
+
+ // In this class we use the empty string to represent 'all locales' and mLocale cannot
+ // be null. However the addWord method takes null to mean 'all locales'.
+ UserDictionaryCompatUtils.addWord(context, newWord.toString(),
+ FREQUENCY_FOR_USER_DICTIONARY_ADDS, newShortcut, TextUtils.isEmpty(mLocale) ?
+ null : LocaleUtils.constructLocaleFromString(mLocale));
+
+ return CODE_WORD_ADDED;
+ }
+
+ private static final String[] HAS_WORD_PROJECTION = { UserDictionary.Words.WORD };
+ private static final String HAS_WORD_SELECTION_ONE_LOCALE = UserDictionary.Words.WORD
+ + "=? AND " + UserDictionary.Words.LOCALE + "=?";
+ private static final String HAS_WORD_SELECTION_ALL_LOCALES = UserDictionary.Words.WORD
+ + "=? AND " + UserDictionary.Words.LOCALE + " is null";
+ private boolean hasWord(final String word, final Context context) {
+ final Cursor cursor;
+ // mLocale == "" indicates this is an entry for all languages. Here, mLocale can't
+ // be null at all (it's ensured by the updateLocale method).
+ if ("".equals(mLocale)) {
+ cursor = context.getContentResolver().query(UserDictionary.Words.CONTENT_URI,
+ HAS_WORD_PROJECTION, HAS_WORD_SELECTION_ALL_LOCALES,
+ new String[] { word }, null /* sort order */);
+ } else {
+ cursor = context.getContentResolver().query(UserDictionary.Words.CONTENT_URI,
+ HAS_WORD_PROJECTION, HAS_WORD_SELECTION_ONE_LOCALE,
+ new String[] { word, mLocale }, null /* sort order */);
+ }
+ try {
+ if (null == cursor) return false;
+ return cursor.getCount() > 0;
+ } finally {
+ if (null != cursor) cursor.close();
+ }
+ }
+
+ public static class LocaleRenderer {
+ private final String mLocaleString;
+ private final String mDescription;
+ // LocaleString may NOT be null.
+ public LocaleRenderer(final Context context, final String localeString) {
+ mLocaleString = localeString;
+ if (null == localeString) {
+ mDescription = context.getString(R.string.user_dict_settings_more_languages);
+ } else if ("".equals(localeString)) {
+ mDescription = context.getString(R.string.user_dict_settings_all_languages);
+ } else {
+ mDescription = LocaleUtils.constructLocaleFromString(localeString).getDisplayName();
+ }
+ }
+ @Override
+ public String toString() {
+ return mDescription;
+ }
+ public String getLocaleString() {
+ return mLocaleString;
+ }
+ // "More languages..." is null ; "All languages" is the empty string.
+ public boolean isMoreLanguages() {
+ return null == mLocaleString;
+ }
+ }
+
+ private static void addLocaleDisplayNameToList(final Context context,
+ final ArrayList<LocaleRenderer> list, final String locale) {
+ if (null != locale) {
+ list.add(new LocaleRenderer(context, locale));
+ }
+ }
+
+ // Helper method to get the list of locales to display for this word
+ public ArrayList<LocaleRenderer> getLocalesList(final Activity activity) {
+ final TreeSet<String> locales = UserDictionaryList.getUserDictionaryLocalesSet(activity);
+ // Remove our locale if it's in, because we're always gonna put it at the top
+ locales.remove(mLocale); // mLocale may not be null
+ final String systemLocale = Locale.getDefault().toString();
+ // The system locale should be inside. We want it at the 2nd spot.
+ locales.remove(systemLocale); // system locale may not be null
+ locales.remove(""); // Remove the empty string if it's there
+ final ArrayList<LocaleRenderer> localesList = new ArrayList<LocaleRenderer>();
+ // Add the passed locale, then the system locale at the top of the list. Add an
+ // "all languages" entry at the bottom of the list.
+ addLocaleDisplayNameToList(activity, localesList, mLocale);
+ if (!systemLocale.equals(mLocale)) {
+ addLocaleDisplayNameToList(activity, localesList, systemLocale);
+ }
+ for (final String l : locales) {
+ // TODO: sort in unicode order
+ addLocaleDisplayNameToList(activity, localesList, l);
+ }
+ if (!"".equals(mLocale)) {
+ // If mLocale is "", then we already inserted the "all languages" item, so don't do it
+ addLocaleDisplayNameToList(activity, localesList, ""); // meaning: all languages
+ }
+ localesList.add(new LocaleRenderer(activity, null)); // meaning: select another locale
+ return localesList;
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/userdictionary/UserDictionaryAddWordFragment.java b/java/src/com/android/inputmethod/latin/userdictionary/UserDictionaryAddWordFragment.java
new file mode 100644
index 000000000..58c8f266c
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/userdictionary/UserDictionaryAddWordFragment.java
@@ -0,0 +1,162 @@
+/*
+ * 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.userdictionary;
+
+import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.userdictionary.UserDictionaryAddWordContents.LocaleRenderer;
+import com.android.inputmethod.latin.userdictionary.UserDictionaryLocalePicker.LocationChangedListener;
+
+import android.app.Fragment;
+import android.os.Bundle;
+import android.preference.PreferenceActivity;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.Spinner;
+
+import java.util.ArrayList;
+import java.util.Locale;
+
+// Caveat: This class is basically taken from
+// packages/apps/Settings/src/com/android/settings/inputmethod/UserDictionaryAddWordFragment.java
+// in order to deal with some devices that have issues with the user dictionary handling
+
+/**
+ * Fragment to add a word/shortcut to the user dictionary.
+ *
+ * As opposed to the UserDictionaryActivity, this is only invoked within Settings
+ * from the UserDictionarySettings.
+ */
+public class UserDictionaryAddWordFragment extends Fragment
+ implements AdapterView.OnItemSelectedListener, LocationChangedListener {
+
+ private static final int OPTIONS_MENU_ADD = Menu.FIRST;
+ private static final int OPTIONS_MENU_DELETE = Menu.FIRST + 1;
+
+ private UserDictionaryAddWordContents mContents;
+ private View mRootView;
+ private boolean mIsDeleting = false;
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ setHasOptionsMenu(true);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
+ mRootView = inflater.inflate(R.layout.user_dictionary_add_word_fullscreen, null);
+ mIsDeleting = false;
+ if (null == mContents) {
+ mContents = new UserDictionaryAddWordContents(mRootView, getArguments());
+ }
+ return mRootView;
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ final MenuItem actionItemAdd = menu.add(0, OPTIONS_MENU_ADD, 0,
+ R.string.user_dict_settings_add_menu_title).setIcon(R.drawable.ic_menu_add);
+ actionItemAdd.setShowAsAction(
+ MenuItem.SHOW_AS_ACTION_IF_ROOM | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
+ final MenuItem actionItemDelete = menu.add(0, OPTIONS_MENU_DELETE, 0,
+ R.string.user_dict_settings_delete).setIcon(android.R.drawable.ic_menu_delete);
+ actionItemDelete.setShowAsAction(
+ MenuItem.SHOW_AS_ACTION_IF_ROOM | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
+ }
+
+ /**
+ * Callback for the framework when a menu option is pressed.
+ *
+ * @param MenuItem the item that was pressed
+ * @return false to allow normal menu processing to proceed, true to consume it here
+ */
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (item.getItemId() == OPTIONS_MENU_ADD) {
+ // added the entry in "onPause"
+ getActivity().onBackPressed();
+ return true;
+ }
+ if (item.getItemId() == OPTIONS_MENU_DELETE) {
+ mContents.delete(getActivity());
+ mIsDeleting = true;
+ getActivity().onBackPressed();
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ // We are being shown: display the word
+ updateSpinner();
+ }
+
+ private void updateSpinner() {
+ final ArrayList<LocaleRenderer> localesList = mContents.getLocalesList(getActivity());
+
+ final Spinner localeSpinner =
+ (Spinner)mRootView.findViewById(R.id.user_dictionary_add_locale);
+ final ArrayAdapter<LocaleRenderer> adapter = new ArrayAdapter<LocaleRenderer>(getActivity(),
+ android.R.layout.simple_spinner_item, localesList);
+ adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ localeSpinner.setAdapter(adapter);
+ localeSpinner.setOnItemSelectedListener(this);
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ // We are being hidden: commit changes to the user dictionary, unless we were deleting it
+ if (!mIsDeleting) {
+ mContents.apply(getActivity(), null);
+ }
+ }
+
+ @Override
+ public void onItemSelected(final AdapterView<?> parent, final View view, final int pos,
+ final long id) {
+ final LocaleRenderer locale = (LocaleRenderer)parent.getItemAtPosition(pos);
+ if (locale.isMoreLanguages()) {
+ PreferenceActivity preferenceActivity = (PreferenceActivity)getActivity();
+ preferenceActivity.startPreferenceFragment(new UserDictionaryLocalePicker(), true);
+ } else {
+ mContents.updateLocale(locale.getLocaleString());
+ }
+ }
+
+ @Override
+ public void onNothingSelected(final AdapterView<?> parent) {
+ // I'm not sure we can come here, but if we do, that's the right thing to do.
+ final Bundle args = getArguments();
+ mContents.updateLocale(args.getString(UserDictionaryAddWordContents.EXTRA_LOCALE));
+ }
+
+ // Called by the locale picker
+ @Override
+ public void onLocaleSelected(final Locale locale) {
+ mContents.updateLocale(locale.toString());
+ getActivity().onBackPressed();
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/userdictionary/UserDictionaryList.java b/java/src/com/android/inputmethod/latin/userdictionary/UserDictionaryList.java
new file mode 100644
index 000000000..6e64882b6
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/userdictionary/UserDictionaryList.java
@@ -0,0 +1,127 @@
+/*
+ * 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.userdictionary;
+
+import com.android.inputmethod.latin.LocaleUtils;
+import com.android.inputmethod.latin.R;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.preference.Preference;
+import android.preference.PreferenceFragment;
+import android.preference.PreferenceGroup;
+import android.provider.UserDictionary;
+import android.text.TextUtils;
+
+import java.util.Locale;
+import java.util.TreeSet;
+
+// Caveat: This class is basically taken from
+// packages/apps/Settings/src/com/android/settings/inputmethod/UserDictionaryList.java
+// in order to deal with some devices that have issues with the user dictionary handling
+
+public class UserDictionaryList extends PreferenceFragment {
+
+ public static final String USER_DICTIONARY_SETTINGS_INTENT_ACTION =
+ "android.settings.USER_DICTIONARY_SETTINGS";
+
+ @Override
+ public void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+ setPreferenceScreen(getPreferenceManager().createPreferenceScreen(getActivity()));
+ }
+
+ public static TreeSet<String> getUserDictionaryLocalesSet(Activity activity) {
+ @SuppressWarnings("deprecation")
+ final Cursor cursor = activity.managedQuery(UserDictionary.Words.CONTENT_URI,
+ new String[] { UserDictionary.Words.LOCALE },
+ null, null, null);
+ final TreeSet<String> localeList = new TreeSet<String>();
+ boolean addedAllLocale = false;
+ if (null == cursor) {
+ // The user dictionary service is not present or disabled. Return null.
+ return null;
+ } else if (cursor.moveToFirst()) {
+ final int columnIndex = cursor.getColumnIndex(UserDictionary.Words.LOCALE);
+ do {
+ final String locale = cursor.getString(columnIndex);
+ final boolean allLocale = TextUtils.isEmpty(locale);
+ localeList.add(allLocale ? "" : locale);
+ if (allLocale) {
+ addedAllLocale = true;
+ }
+ } while (cursor.moveToNext());
+ }
+ if (!UserDictionarySettings.IS_SHORTCUT_API_SUPPORTED && !addedAllLocale) {
+ // For ICS, we need to show "For all languages" in case that the keyboard locale
+ // is different from the system locale
+ localeList.add("");
+ }
+ localeList.add(Locale.getDefault().toString());
+ return localeList;
+ }
+
+ /**
+ * Creates the entries that allow the user to go into the user dictionary for each locale.
+ * @param userDictGroup The group to put the settings in.
+ */
+ protected void createUserDictSettings(PreferenceGroup userDictGroup) {
+ final Activity activity = getActivity();
+ userDictGroup.removeAll();
+ final TreeSet<String> localeList =
+ UserDictionaryList.getUserDictionaryLocalesSet(activity);
+
+ if (localeList.isEmpty()) {
+ userDictGroup.addPreference(createUserDictionaryPreference(null, activity));
+ } else {
+ for (String locale : localeList) {
+ userDictGroup.addPreference(createUserDictionaryPreference(locale, activity));
+ }
+ }
+ }
+
+ /**
+ * Create a single User Dictionary Preference object, with its parameters set.
+ * @param locale The locale for which this user dictionary is for.
+ * @return The corresponding preference.
+ */
+ protected Preference createUserDictionaryPreference(String locale, Activity activity) {
+ final Preference newPref = new Preference(getActivity());
+ final Intent intent = new Intent(USER_DICTIONARY_SETTINGS_INTENT_ACTION);
+ if (null == locale) {
+ newPref.setTitle(Locale.getDefault().getDisplayName());
+ } else {
+ if ("".equals(locale))
+ newPref.setTitle(getString(R.string.user_dict_settings_all_languages));
+ else
+ newPref.setTitle(LocaleUtils.constructLocaleFromString(locale).getDisplayName());
+ intent.putExtra("locale", locale);
+ newPref.getExtras().putString("locale", locale);
+ }
+ newPref.setIntent(intent);
+ newPref.setFragment(UserDictionarySettings.class.getName());
+ return newPref;
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ createUserDictSettings(getPreferenceScreen());
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/userdictionary/UserDictionaryLocalePicker.java b/java/src/com/android/inputmethod/latin/userdictionary/UserDictionaryLocalePicker.java
new file mode 100644
index 000000000..58d3fb91c
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/userdictionary/UserDictionaryLocalePicker.java
@@ -0,0 +1,36 @@
+/*
+ * 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.userdictionary;
+
+import android.app.Fragment;
+
+import java.util.Locale;
+
+// Caveat: This class is basically taken from
+// packages/apps/Settings/src/com/android/settings/inputmethod/UserDictionaryLocalePicker.java
+// in order to deal with some devices that have issues with the user dictionary handling
+
+public class UserDictionaryLocalePicker extends Fragment {
+ public UserDictionaryLocalePicker() {
+ super();
+ // TODO: implement
+ }
+
+ public interface LocationChangedListener {
+ public void onLocaleSelected(Locale locale);
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/userdictionary/UserDictionarySettings.java b/java/src/com/android/inputmethod/latin/userdictionary/UserDictionarySettings.java
new file mode 100644
index 000000000..50dda9663
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/userdictionary/UserDictionarySettings.java
@@ -0,0 +1,334 @@
+/**
+ * Copyright (C) 2013 Google Inc.
+ *
+ * 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.userdictionary;
+
+import com.android.inputmethod.latin.R;
+
+import android.app.ListFragment;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.os.Build;
+import android.os.Bundle;
+import android.provider.UserDictionary;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AlphabetIndexer;
+import android.widget.ListAdapter;
+import android.widget.ListView;
+import android.widget.SectionIndexer;
+import android.widget.SimpleCursorAdapter;
+import android.widget.TextView;
+
+import java.util.Locale;
+
+// Caveat: This class is basically taken from
+// packages/apps/Settings/src/com/android/settings/inputmethod/UserDictionarySettings.java
+// in order to deal with some devices that have issues with the user dictionary handling
+
+public class UserDictionarySettings extends ListFragment {
+
+ public static final boolean IS_SHORTCUT_API_SUPPORTED =
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN;
+
+ private static final String[] QUERY_PROJECTION_SHORTCUT_UNSUPPORTED =
+ { UserDictionary.Words._ID, UserDictionary.Words.WORD};
+ private static final String[] QUERY_PROJECTION_SHORTCUT_SUPPORTED =
+ { UserDictionary.Words._ID, UserDictionary.Words.WORD, UserDictionary.Words.SHORTCUT};
+ private static final String[] QUERY_PROJECTION =
+ IS_SHORTCUT_API_SUPPORTED ?
+ QUERY_PROJECTION_SHORTCUT_SUPPORTED : QUERY_PROJECTION_SHORTCUT_UNSUPPORTED;
+
+ // The index of the shortcut in the above array.
+ private static final int INDEX_SHORTCUT = 2;
+
+ private static final String[] ADAPTER_FROM_SHORTCUT_UNSUPPORTED = {
+ UserDictionary.Words.WORD,
+ };
+
+ private static final String[] ADAPTER_FROM_SHORTCUT_SUPPORTED = {
+ UserDictionary.Words.WORD, UserDictionary.Words.SHORTCUT
+ };
+
+ private static final String[] ADAPTER_FROM = IS_SHORTCUT_API_SUPPORTED ?
+ ADAPTER_FROM_SHORTCUT_SUPPORTED : ADAPTER_FROM_SHORTCUT_UNSUPPORTED;
+
+ private static final int[] ADAPTER_TO_SHORTCUT_UNSUPPORTED = {
+ android.R.id.text1,
+ };
+
+ private static final int[] ADAPTER_TO_SHORTCUT_SUPPORTED = {
+ android.R.id.text1, android.R.id.text2
+ };
+
+ private static final int[] ADAPTER_TO = IS_SHORTCUT_API_SUPPORTED ?
+ ADAPTER_TO_SHORTCUT_SUPPORTED : ADAPTER_TO_SHORTCUT_UNSUPPORTED;
+
+ // Either the locale is empty (means the word is applicable to all locales)
+ // or the word equals our current locale
+ private static final String QUERY_SELECTION =
+ UserDictionary.Words.LOCALE + "=?";
+ private static final String QUERY_SELECTION_ALL_LOCALES =
+ UserDictionary.Words.LOCALE + " is null";
+
+ private static final String DELETE_SELECTION_WITH_SHORTCUT = UserDictionary.Words.WORD
+ + "=? AND " + UserDictionary.Words.SHORTCUT + "=?";
+ private static final String DELETE_SELECTION_WITHOUT_SHORTCUT = UserDictionary.Words.WORD
+ + "=? AND " + UserDictionary.Words.SHORTCUT + " is null OR "
+ + UserDictionary.Words.SHORTCUT + "=''";
+ private static final String DELETE_SELECTION_SHORTCUT_UNSUPPORTED =
+ UserDictionary.Words.WORD + "=?";
+
+ private static final int OPTIONS_MENU_ADD = Menu.FIRST;
+
+ private Cursor mCursor;
+
+ protected String mLocale;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ getActivity().getActionBar().setTitle(R.string.edit_personal_dictionary);
+ }
+
+ @Override
+ public View onCreateView(
+ LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ return inflater.inflate(
+ R.layout.user_dictionary_preference_list_fragment, container, false);
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ final Intent intent = getActivity().getIntent();
+ final String localeFromIntent =
+ null == intent ? null : intent.getStringExtra("locale");
+
+ final Bundle arguments = getArguments();
+ final String localeFromArguments =
+ null == arguments ? null : arguments.getString("locale");
+
+ final String locale;
+ if (null != localeFromArguments) {
+ locale = localeFromArguments;
+ } else if (null != localeFromIntent) {
+ locale = localeFromIntent;
+ } else {
+ locale = null;
+ }
+
+ mLocale = locale;
+ mCursor = createCursor(locale);
+ TextView emptyView = (TextView) getView().findViewById(android.R.id.empty);
+ emptyView.setText(R.string.user_dict_settings_empty_text);
+
+ final ListView listView = getListView();
+ listView.setAdapter(createAdapter());
+ listView.setFastScrollEnabled(true);
+ listView.setEmptyView(emptyView);
+
+ setHasOptionsMenu(true);
+
+ }
+
+ @SuppressWarnings("deprecation")
+ private Cursor createCursor(final String locale) {
+ // Locale can be any of:
+ // - The string representation of a locale, as returned by Locale#toString()
+ // - The empty string. This means we want a cursor returning words valid for all locales.
+ // - null. This means we want a cursor for the current locale, whatever this is.
+ // Note that this contrasts with the data inside the database, where NULL means "all
+ // locales" and there should never be an empty string. The confusion is called by the
+ // historical use of null for "all locales".
+ // TODO: it should be easy to make this more readable by making the special values
+ // human-readable, like "all_locales" and "current_locales" strings, provided they
+ // can be guaranteed not to match locales that may exist.
+ if ("".equals(locale)) {
+ // Case-insensitive sort
+ return getActivity().managedQuery(UserDictionary.Words.CONTENT_URI, QUERY_PROJECTION,
+ QUERY_SELECTION_ALL_LOCALES, null,
+ "UPPER(" + UserDictionary.Words.WORD + ")");
+ } else {
+ final String queryLocale = null != locale ? locale : Locale.getDefault().toString();
+ return getActivity().managedQuery(UserDictionary.Words.CONTENT_URI, QUERY_PROJECTION,
+ QUERY_SELECTION, new String[] { queryLocale },
+ "UPPER(" + UserDictionary.Words.WORD + ")");
+ }
+ }
+
+ private ListAdapter createAdapter() {
+ return new MyAdapter(getActivity(), R.layout.user_dictionary_item, mCursor,
+ ADAPTER_FROM, ADAPTER_TO, this);
+ }
+
+ @Override
+ public void onListItemClick(ListView l, View v, int position, long id) {
+ final String word = getWord(position);
+ final String shortcut = getShortcut(position);
+ if (word != null) {
+ showAddOrEditDialog(word, shortcut);
+ }
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ if (!UserDictionarySettings.IS_SHORTCUT_API_SUPPORTED) {
+ final Locale systemLocale = getResources().getConfiguration().locale;
+ if (!TextUtils.isEmpty(mLocale) && !mLocale.equals(systemLocale.toString())) {
+ // Hide the add button for ICS because it doesn't support specifying a locale
+ // for an entry. This new "locale"-aware API has been added in conjunction
+ // with the shortcut API.
+ return;
+ }
+ }
+ MenuItem actionItem =
+ menu.add(0, OPTIONS_MENU_ADD, 0, R.string.user_dict_settings_add_menu_title)
+ .setIcon(R.drawable.ic_menu_add);
+ actionItem.setShowAsAction(
+ MenuItem.SHOW_AS_ACTION_IF_ROOM | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (item.getItemId() == OPTIONS_MENU_ADD) {
+ showAddOrEditDialog(null, null);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Add or edit a word. If editingWord is null, it's an add; otherwise, it's an edit.
+ * @param editingWord the word to edit, or null if it's an add.
+ * @param editingShortcut the shortcut for this entry, or null if none.
+ */
+ private void showAddOrEditDialog(final String editingWord, final String editingShortcut) {
+ final Bundle args = new Bundle();
+ args.putInt(UserDictionaryAddWordContents.EXTRA_MODE, null == editingWord
+ ? UserDictionaryAddWordContents.MODE_INSERT
+ : UserDictionaryAddWordContents.MODE_EDIT);
+ args.putString(UserDictionaryAddWordContents.EXTRA_WORD, editingWord);
+ args.putString(UserDictionaryAddWordContents.EXTRA_SHORTCUT, editingShortcut);
+ args.putString(UserDictionaryAddWordContents.EXTRA_LOCALE, mLocale);
+ android.preference.PreferenceActivity pa =
+ (android.preference.PreferenceActivity)getActivity();
+ pa.startPreferencePanel(UserDictionaryAddWordFragment.class.getName(),
+ args, R.string.user_dict_settings_add_dialog_title, null, null, 0);
+ }
+
+ private String getWord(final int position) {
+ if (null == mCursor) return null;
+ mCursor.moveToPosition(position);
+ // Handle a possible race-condition
+ if (mCursor.isAfterLast()) return null;
+
+ return mCursor.getString(
+ mCursor.getColumnIndexOrThrow(UserDictionary.Words.WORD));
+ }
+
+ private String getShortcut(final int position) {
+ if (!IS_SHORTCUT_API_SUPPORTED) return null;
+ if (null == mCursor) return null;
+ mCursor.moveToPosition(position);
+ // Handle a possible race-condition
+ if (mCursor.isAfterLast()) return null;
+
+ return mCursor.getString(
+ mCursor.getColumnIndexOrThrow(UserDictionary.Words.SHORTCUT));
+ }
+
+ public static void deleteWord(final String word, final String shortcut,
+ final ContentResolver resolver) {
+ if (!IS_SHORTCUT_API_SUPPORTED) {
+ resolver.delete(UserDictionary.Words.CONTENT_URI, DELETE_SELECTION_SHORTCUT_UNSUPPORTED,
+ new String[] { word });
+ } else if (TextUtils.isEmpty(shortcut)) {
+ resolver.delete(
+ UserDictionary.Words.CONTENT_URI, DELETE_SELECTION_WITHOUT_SHORTCUT,
+ new String[] { word });
+ } else {
+ resolver.delete(
+ UserDictionary.Words.CONTENT_URI, DELETE_SELECTION_WITH_SHORTCUT,
+ new String[] { word, shortcut });
+ }
+ }
+
+ private static class MyAdapter extends SimpleCursorAdapter implements SectionIndexer {
+
+ private AlphabetIndexer mIndexer;
+
+ private ViewBinder mViewBinder = new ViewBinder() {
+
+ @Override
+ public boolean setViewValue(View v, Cursor c, int columnIndex) {
+ if (!IS_SHORTCUT_API_SUPPORTED) {
+ // just let SimpleCursorAdapter set the view values
+ return false;
+ }
+ if (columnIndex == INDEX_SHORTCUT) {
+ final String shortcut = c.getString(INDEX_SHORTCUT);
+ if (TextUtils.isEmpty(shortcut)) {
+ v.setVisibility(View.GONE);
+ } else {
+ ((TextView)v).setText(shortcut);
+ v.setVisibility(View.VISIBLE);
+ }
+ v.invalidate();
+ return true;
+ }
+
+ return false;
+ }
+ };
+
+ @SuppressWarnings("deprecation")
+ public MyAdapter(Context context, int layout, Cursor c, String[] from, int[] to,
+ UserDictionarySettings settings) {
+ super(context, layout, c, from, to);
+
+ if (null != c) {
+ final String alphabet = context.getString(R.string.user_dict_fast_scroll_alphabet);
+ final int wordColIndex = c.getColumnIndexOrThrow(UserDictionary.Words.WORD);
+ mIndexer = new AlphabetIndexer(c, wordColIndex, alphabet);
+ }
+ setViewBinder(mViewBinder);
+ }
+
+ @Override
+ public int getPositionForSection(int section) {
+ return null == mIndexer ? 0 : mIndexer.getPositionForSection(section);
+ }
+
+ @Override
+ public int getSectionForPosition(int position) {
+ return null == mIndexer ? 0 : mIndexer.getSectionForPosition(position);
+ }
+
+ @Override
+ public Object[] getSections() {
+ return null == mIndexer ? null : mIndexer.getSections();
+ }
+ }
+}
diff --git a/java/src/com/android/inputmethod/research/FeedbackFragment.java b/java/src/com/android/inputmethod/research/FeedbackFragment.java
index 39f9c87a0..a0738292e 100644
--- a/java/src/com/android/inputmethod/research/FeedbackFragment.java
+++ b/java/src/com/android/inputmethod/research/FeedbackFragment.java
@@ -65,12 +65,10 @@ public class FeedbackFragment extends Fragment implements OnClickListener {
mCancelButton.setOnClickListener(this);
if (savedInstanceState != null) {
- Log.d(TAG, "restoring from savedInstanceState");
restoreState(savedInstanceState);
} else {
final Bundle bundle = getActivity().getIntent().getExtras();
if (bundle != null) {
- Log.d(TAG, "restoring from getArguments()");
restoreState(bundle);
}
}
@@ -81,10 +79,7 @@ public class FeedbackFragment extends Fragment implements OnClickListener {
public void onClick(final View view) {
final ResearchLogger researchLogger = ResearchLogger.getInstance();
if (view == mIncludingUserRecordingCheckBox) {
- if (hasUserRecording()) {
- // Remove the recording
- setHasUserRecording(false);
- } else {
+ if (mIncludingUserRecordingCheckBox.isChecked()) {
final Bundle bundle = new Bundle();
onSaveInstanceState(bundle);
@@ -103,9 +98,9 @@ public class FeedbackFragment extends Fragment implements OnClickListener {
R.string.research_feedback_empty_feedback_error_message,
Toast.LENGTH_LONG).show();
} else {
- final boolean isIncludingAccountName = isIncludingAccountName();
- researchLogger.sendFeedback(feedbackContents,
- false /* isIncludingHistory */, isIncludingAccountName, hasUserRecording());
+ final boolean isIncludingAccountName = mIncludingAccountNameCheckBox.isChecked();
+ researchLogger.sendFeedback(feedbackContents, false /* isIncludingHistory */,
+ isIncludingAccountName, mIncludingUserRecordingCheckBox.isChecked());
getActivity().finish();
researchLogger.setFeedbackDialogBundle(null);
researchLogger.onLeavingSendFeedbackDialog();
@@ -125,29 +120,13 @@ public class FeedbackFragment extends Fragment implements OnClickListener {
final String savedFeedbackString = mEditText.getText().toString();
bundle.putString(KEY_FEEDBACK_STRING, savedFeedbackString);
- bundle.putBoolean(KEY_INCLUDE_ACCOUNT_NAME, isIncludingAccountName());
- bundle.putBoolean(KEY_HAS_USER_RECORDING, hasUserRecording());
+ bundle.putBoolean(KEY_INCLUDE_ACCOUNT_NAME, mIncludingAccountNameCheckBox.isChecked());
+ bundle.putBoolean(KEY_HAS_USER_RECORDING, mIncludingUserRecordingCheckBox.isChecked());
}
- public void restoreState(final Bundle bundle) {
+ private void restoreState(final Bundle bundle) {
mEditText.setText(bundle.getString(KEY_FEEDBACK_STRING));
- setIsIncludingAccountName(bundle.getBoolean(KEY_INCLUDE_ACCOUNT_NAME));
- setHasUserRecording(bundle.getBoolean(KEY_HAS_USER_RECORDING));
- }
-
- private boolean hasUserRecording() {
- return mIncludingUserRecordingCheckBox.isChecked();
- }
-
- private void setHasUserRecording(final boolean hasRecording) {
- mIncludingUserRecordingCheckBox.setChecked(hasRecording);
- }
-
- private boolean isIncludingAccountName() {
- return mIncludingAccountNameCheckBox.isChecked();
- }
-
- private void setIsIncludingAccountName(final boolean isIncludingAccountName) {
- mIncludingAccountNameCheckBox.setChecked(isIncludingAccountName);
+ mIncludingAccountNameCheckBox.setChecked(bundle.getBoolean(KEY_INCLUDE_ACCOUNT_NAME));
+ mIncludingUserRecordingCheckBox.setChecked(bundle.getBoolean(KEY_HAS_USER_RECORDING));
}
}
diff --git a/java/src/com/android/inputmethod/research/FixedLogBuffer.java b/java/src/com/android/inputmethod/research/FixedLogBuffer.java
index 78dc59562..8b64de8ae 100644
--- a/java/src/com/android/inputmethod/research/FixedLogBuffer.java
+++ b/java/src/com/android/inputmethod/research/FixedLogBuffer.java
@@ -51,38 +51,36 @@ public class FixedLogBuffer extends LogBuffer {
mNumActualWords = 0;
}
- protected int getNumActualWords() {
- return mNumActualWords;
- }
-
/**
* Adds a new LogUnit to the front of the LIFO queue, evicting existing LogUnit's
* (oldest first) if word capacity is reached.
*/
@Override
public void shiftIn(final LogUnit newLogUnit) {
- if (!newLogUnit.hasWord()) {
- // This LogUnit isn't a word, so it doesn't count toward the word-limit.
+ if (!newLogUnit.hasOneOrMoreWords()) {
+ // This LogUnit doesn't contain any word, so it doesn't count toward the word-limit.
super.shiftIn(newLogUnit);
return;
}
+ final int numWordsIncoming = newLogUnit.getNumWords();
if (mNumActualWords >= mWordCapacity) {
// Give subclass a chance to handle the buffer full condition by shifting out logUnits.
+ // TODO: Tell onBufferFull() how much space it needs to make to avoid forced eviction.
onBufferFull();
// If still full, evict.
if (mNumActualWords >= mWordCapacity) {
- shiftOutWords(1);
+ shiftOutWords(numWordsIncoming);
}
}
super.shiftIn(newLogUnit);
- mNumActualWords++; // Must be a word, or we wouldn't be here.
+ mNumActualWords += numWordsIncoming;
}
@Override
public LogUnit unshiftIn() {
final LogUnit logUnit = super.unshiftIn();
- if (logUnit != null && logUnit.hasWord()) {
- mNumActualWords--;
+ if (logUnit != null && logUnit.hasOneOrMoreWords()) {
+ mNumActualWords -= logUnit.getNumWords();
}
return logUnit;
}
@@ -113,18 +111,28 @@ public class FixedLogBuffer extends LogBuffer {
@Override
public LogUnit shiftOut() {
final LogUnit logUnit = super.shiftOut();
- if (logUnit != null && logUnit.hasWord()) {
- mNumActualWords--;
+ if (logUnit != null && logUnit.hasOneOrMoreWords()) {
+ mNumActualWords -= logUnit.getNumWords();
}
return logUnit;
}
- protected void shiftOutWords(final int numWords) {
- final int targetNumWords = mNumActualWords - numWords;
- final LinkedList<LogUnit> logUnits = getLogUnits();
- while (mNumActualWords > targetNumWords && !logUnits.isEmpty()) {
- shiftOut();
- }
+ /**
+ * Remove LogUnits from the front of the LogBuffer until {@code numWords} have been removed.
+ *
+ * If there are less than {@code numWords} in the buffer, shifts out all {@code LogUnit}s.
+ *
+ * @param numWords the minimum number of words in {@link LogUnit}s to shift out
+ * @return the number of actual words LogUnit}s shifted out
+ */
+ protected int shiftOutWords(final int numWords) {
+ int numWordsShiftedOut = 0;
+ do {
+ final LogUnit logUnit = shiftOut();
+ if (logUnit == null) break;
+ numWordsShiftedOut += logUnit.getNumWords();
+ } while (numWordsShiftedOut < numWords);
+ return numWordsShiftedOut;
}
public void shiftOutAll() {
@@ -136,27 +144,31 @@ public class FixedLogBuffer extends LogBuffer {
}
/**
- * Returns a list of {@link LogUnit}s at the front of the buffer that have associated words. No
- * more than {@code n} LogUnits will have words associated with them. If there are not enough
- * LogUnits in the buffer to meet the word requirement, returns the all LogUnits.
+ * Returns a list of {@link LogUnit}s at the front of the buffer that have words associated with
+ * them.
+ *
+ * There will be no more than {@code n} words in the returned list. So if 2 words are
+ * requested, and the first LogUnit has 3 words, it is not returned. If 2 words are requested,
+ * and the first LogUnit has only 1 word, and the next LogUnit 2 words, only the first LogUnit
+ * is returned. If the first LogUnit has no words associated with it, and the second LogUnit
+ * has three words, then only the first LogUnit (which has no associated words) is returned. If
+ * there are not enough LogUnits in the buffer to meet the word requirement, then all LogUnits
+ * will be returned.
*
* @param n The maximum number of {@link LogUnit}s with words to return.
* @return The list of the {@link LogUnit}s containing the first n words
*/
public ArrayList<LogUnit> peekAtFirstNWords(int n) {
final LinkedList<LogUnit> logUnits = getLogUnits();
- final int length = logUnits.size();
// Allocate space for n*2 logUnits. There will be at least n, one for each word, and
// there may be additional for punctuation, between-word commands, etc. This should be
// enough that reallocation won't be necessary.
- final ArrayList<LogUnit> list = new ArrayList<LogUnit>(n * 2);
- for (int i = 0; i < length && n > 0; i++) {
- final LogUnit logUnit = logUnits.get(i);
- list.add(logUnit);
- if (logUnit.hasWord()) {
- n--;
- }
+ final ArrayList<LogUnit> resultList = new ArrayList<LogUnit>(n * 2);
+ for (final LogUnit logUnit : logUnits) {
+ n -= logUnit.getNumWords();
+ if (n < 0) break;
+ resultList.add(logUnit);
}
- return list;
+ return resultList;
}
}
diff --git a/java/src/com/android/inputmethod/research/LogUnit.java b/java/src/com/android/inputmethod/research/LogUnit.java
index 1c01675bd..cf1388f46 100644
--- a/java/src/com/android/inputmethod/research/LogUnit.java
+++ b/java/src/com/android/inputmethod/research/LogUnit.java
@@ -26,9 +26,10 @@ import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
import com.android.inputmethod.latin.define.ProductionFlag;
import java.io.IOException;
-import java.io.StringWriter;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
+import java.util.regex.Pattern;
/**
* A group of log statements related to each other.
@@ -49,27 +50,45 @@ public class LogUnit {
private static final boolean DEBUG = false
&& ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS_DEBUG;
+ private static final Pattern WHITESPACE_PATTERN = Pattern.compile("\\s+");
+ private static final String[] EMPTY_STRING_ARRAY = new String[0];
+
private final ArrayList<LogStatement> mLogStatementList;
private final ArrayList<Object[]> mValuesList;
// Assume that mTimeList is sorted in increasing order. Do not insert null values into
// mTimeList.
private final ArrayList<Long> mTimeList;
- // Word that this LogUnit generates. Should be null if the LogUnit does not generate a genuine
- // word (i.e. separators alone do not count as a word). Should never be empty.
- private String mWord;
+ // Words that this LogUnit generates. Should be null if the data in the LogUnit does not
+ // generate a genuine word (i.e. separators alone do not count as a word). Should never be
+ // empty. Note that if the user types spaces explicitly, then normally mWords should contain
+ // only a single word; it will only contain space-separate multiple words if the user does not
+ // enter a space, and the system enters one automatically.
+ private String mWords;
+ private String[] mWordArray = EMPTY_STRING_ARRAY;
private boolean mMayContainDigit;
private boolean mIsPartOfMegaword;
private boolean mContainsCorrection;
- // mCorrectionType indicates whether the word was corrected at all, and if so, whether it was
- // to a different word or just a "typo" correction. It is considered a "typo" if the final
- // word was listed in the suggestions available the first time the word was gestured or
- // tapped.
+ // mCorrectionType indicates whether the word was corrected at all, and if so, the nature of the
+ // correction.
private int mCorrectionType;
+ // LogUnits start in this state. If a word is entered without being corrected, it will have
+ // this CorrectiontType.
public static final int CORRECTIONTYPE_NO_CORRECTION = 0;
+ // The LogUnit was corrected manually by the user in an unspecified way.
public static final int CORRECTIONTYPE_CORRECTION = 1;
+ // The LogUnit was corrected manually by the user to a word not in the list of suggestions of
+ // the first word typed here. (Note: this is a heuristic value, it may be incorrect, for
+ // example, if the user repositions the cursor).
public static final int CORRECTIONTYPE_DIFFERENT_WORD = 2;
+ // The LogUnit was corrected manually by the user to a word that was in the list of suggestions
+ // of the first word typed here. (Again, a heuristic). It is probably a typo correction.
public static final int CORRECTIONTYPE_TYPO = 3;
+ // TODO: Rather than just tracking the current state, keep a historical record of the LogUnit's
+ // state and statistics. This should include how many times it has been corrected, whether
+ // other LogUnit edits were done between edits to this LogUnit, etc. Also track when a LogUnit
+ // previously contained a word, but was corrected to empty (because it was deleted, and there is
+ // no known replacement).
private SuggestedWords mSuggestedWords;
@@ -117,9 +136,11 @@ public class LogUnit {
* @param researchLog where to publish the contents of this {@code LogUnit}
* @param canIncludePrivateData whether the private data in this {@code LogUnit} should be
* included
+ *
+ * @throws IOException if publication to the log file is not possible
*/
public synchronized void publishTo(final ResearchLog researchLog,
- final boolean canIncludePrivateData) {
+ final boolean canIncludePrivateData) throws IOException {
// Write out any logStatement that passes the privacy filter.
final int size = mLogStatementList.size();
if (size != 0) {
@@ -166,7 +187,7 @@ public class LogUnit {
final LogStatement logStatement;
if (canIncludePrivateData) {
LOGSTATEMENT_LOG_UNIT_BEGIN_WITH_PRIVATE_DATA.outputToLocked(jsonWriter,
- SystemClock.uptimeMillis(), getWord(), getCorrectionType());
+ SystemClock.uptimeMillis(), getWordsAsString(), getCorrectionType());
} else {
LOGSTATEMENT_LOG_UNIT_BEGIN_WITHOUT_PRIVATE_DATA.outputToLocked(jsonWriter,
SystemClock.uptimeMillis());
@@ -181,22 +202,22 @@ public class LogUnit {
}
/**
- * Mark the current logUnit as containing data to generate {@code word}.
+ * Mark the current logUnit as containing data to generate {@code newWords}.
*
* If {@code setWord()} was previously called for this LogUnit, then the method will try to
* determine what kind of correction it is, and update its internal state of the correctionType
* accordingly.
*
- * @param word The word this LogUnit generates. Caller should not pass null or the empty
+ * @param newWords The words this LogUnit generates. Caller should not pass null or the empty
* string.
*/
- public void setWord(final String word) {
- if (hasWord()) {
+ public void setWords(final String newWords) {
+ if (hasOneOrMoreWords()) {
// The word was already set once, and it is now being changed. See if the new word
// is close to the old word. If so, then the change is probably a typo correction.
// If not, the user may have decided to enter a different word, so flag it.
if (mSuggestedWords != null) {
- if (isInSuggestedWords(word, mSuggestedWords)) {
+ if (isInSuggestedWords(newWords, mSuggestedWords)) {
mCorrectionType = CORRECTIONTYPE_TYPO;
} else {
mCorrectionType = CORRECTIONTYPE_DIFFERENT_WORD;
@@ -206,38 +227,71 @@ public class LogUnit {
// Mark it as a generic correction.
mCorrectionType = CORRECTIONTYPE_CORRECTION;
}
+ } else {
+ mCorrectionType = CORRECTIONTYPE_NO_CORRECTION;
}
- mWord = word;
+ mWords = newWords;
+
+ // Update mWordArray
+ mWordArray = (TextUtils.isEmpty(mWords)) ? EMPTY_STRING_ARRAY
+ : WHITESPACE_PATTERN.split(mWords);
+ if (mWordArray.length > 0 && TextUtils.isEmpty(mWordArray[0])) {
+ // Empty string at beginning of array. Must have been whitespace at the start of the
+ // word. Remove the empty string.
+ mWordArray = Arrays.copyOfRange(mWordArray, 1, mWordArray.length);
+ }
+ }
+
+ public String getWordsAsString() {
+ return mWords;
+ }
+
+ /**
+ * Retuns the words generated by the data in this LogUnit.
+ *
+ * The first word may be an empty string, if the data in the LogUnit started by generating
+ * whitespace.
+ *
+ * @return the array of words. an empty list of there are no words associated with this LogUnit.
+ */
+ public String[] getWordsAsStringArray() {
+ return mWordArray;
}
- public String getWord() {
- return mWord;
+ public boolean hasOneOrMoreWords() {
+ return mWordArray.length >= 1;
}
- public boolean hasWord() {
- return mWord != null && !TextUtils.isEmpty(mWord.trim());
+ public int getNumWords() {
+ return mWordArray.length;
}
+ // TODO: Refactor to eliminate getter/setters
public void setMayContainDigit() {
mMayContainDigit = true;
}
+ // TODO: Refactor to eliminate getter/setters
public boolean mayContainDigit() {
return mMayContainDigit;
}
+ // TODO: Refactor to eliminate getter/setters
public void setContainsCorrection() {
mContainsCorrection = true;
}
+ // TODO: Refactor to eliminate getter/setters
public boolean containsCorrection() {
return mContainsCorrection;
}
+ // TODO: Refactor to eliminate getter/setters
public void setCorrectionType(final int correctionType) {
mCorrectionType = correctionType;
}
+ // TODO: Refactor to eliminate getter/setters
public int getCorrectionType() {
return mCorrectionType;
}
@@ -267,7 +321,7 @@ public class LogUnit {
new ArrayList<Object[]>(laterValues),
new ArrayList<Long>(laterTimes),
true /* isPartOfMegaword */);
- newLogUnit.mWord = null;
+ newLogUnit.mWords = null;
newLogUnit.mMayContainDigit = mMayContainDigit;
newLogUnit.mContainsCorrection = mContainsCorrection;
@@ -287,9 +341,9 @@ public class LogUnit {
mLogStatementList.addAll(logUnit.mLogStatementList);
mValuesList.addAll(logUnit.mValuesList);
mTimeList.addAll(logUnit.mTimeList);
- mWord = null;
- if (logUnit.mWord != null) {
- setWord(logUnit.mWord);
+ mWords = null;
+ if (logUnit.mWords != null) {
+ setWords(logUnit.mWords);
}
mMayContainDigit = mMayContainDigit || logUnit.mMayContainDigit;
mContainsCorrection = mContainsCorrection || logUnit.mContainsCorrection;
diff --git a/java/src/com/android/inputmethod/research/MainLogBuffer.java b/java/src/com/android/inputmethod/research/MainLogBuffer.java
index 3303d2bdb..9aa349906 100644
--- a/java/src/com/android/inputmethod/research/MainLogBuffer.java
+++ b/java/src/com/android/inputmethod/research/MainLogBuffer.java
@@ -23,9 +23,9 @@ import com.android.inputmethod.latin.Dictionary;
import com.android.inputmethod.latin.Suggest;
import com.android.inputmethod.latin.define.ProductionFlag;
+import java.io.IOException;
import java.util.ArrayList;
import java.util.LinkedList;
-import java.util.Random;
/**
* MainLogBuffer is a FixedLogBuffer that tracks the state of LogUnits to make privacy guarantees.
@@ -100,10 +100,6 @@ public abstract class MainLogBuffer extends FixedLogBuffer {
return mSuggest.getMainDictionary();
}
- public void resetWordCounter() {
- mNumWordsUntilSafeToSample = mNumWordsBetweenNGrams;
- }
-
public void setIsStopping() {
mIsStopping = true;
}
@@ -131,10 +127,7 @@ public abstract class MainLogBuffer extends FixedLogBuffer {
final int length = logUnits.size();
for (int i = 0; i < length; i++) {
final LogUnit logUnit = logUnits.get(i);
- final String word = logUnit.getWord();
- if (word != null) {
- numWordsInLogUnitList++;
- }
+ numWordsInLogUnitList += logUnit.getNumWords();
}
return numWordsInLogUnitList >= minNGramSize;
}
@@ -158,32 +151,34 @@ public abstract class MainLogBuffer extends FixedLogBuffer {
// the complete buffer contents in detail.
int numWordsInLogUnitList = 0;
final int length = logUnits.size();
- for (int i = 0; i < length; i++) {
- final LogUnit logUnit = logUnits.get(i);
- if (!logUnit.hasWord()) {
+ for (final LogUnit logUnit : logUnits) {
+ if (!logUnit.hasOneOrMoreWords()) {
// Digits outside words are a privacy threat.
if (logUnit.mayContainDigit()) {
return false;
}
} else {
- numWordsInLogUnitList++;
- final String word = logUnit.getWord();
- // Words not in the dictionary are a privacy threat.
- if (ResearchLogger.hasLetters(word) && !(dictionary.isValidWord(word))) {
- if (DEBUG) {
- Log.d(TAG, "NOT SAFE!: hasLetters: " + ResearchLogger.hasLetters(word)
- + ", isValid: " + (dictionary.isValidWord(word)));
+ numWordsInLogUnitList += logUnit.getNumWords();
+ final String[] words = logUnit.getWordsAsStringArray();
+ for (final String word : words) {
+ // Words not in the dictionary are a privacy threat.
+ if (ResearchLogger.hasLetters(word) && !(dictionary.isValidWord(word))) {
+ if (DEBUG) {
+ Log.d(TAG, "\"" + word + "\" NOT SAFE!: hasLetters: "
+ + ResearchLogger.hasLetters(word)
+ + ", isValid: " + (dictionary.isValidWord(word)));
+ }
+ return false;
}
- return false;
}
}
}
- // Finally, only return true if the minNGramSize is met.
- return numWordsInLogUnitList >= minNGramSize;
+ // Finally, only return true if the ngram is the right size.
+ return numWordsInLogUnitList == minNGramSize;
}
- public void shiftAndPublishAll() {
+ public void shiftAndPublishAll() throws IOException {
final LinkedList<LogUnit> logUnits = getLogUnits();
while (!logUnits.isEmpty()) {
publishLogUnitsAtFrontOfBuffer();
@@ -192,23 +187,40 @@ public abstract class MainLogBuffer extends FixedLogBuffer {
@Override
protected final void onBufferFull() {
- publishLogUnitsAtFrontOfBuffer();
+ try {
+ publishLogUnitsAtFrontOfBuffer();
+ } catch (final IOException e) {
+ if (DEBUG) {
+ Log.w(TAG, "IOException when publishing front of LogBuffer", e);
+ }
+ }
}
- protected final void publishLogUnitsAtFrontOfBuffer() {
+ 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)) {
// Good n-gram at the front of the buffer. Publish it, disclosing details.
publish(logUnits, true /* canIncludePrivateData */);
shiftOutWords(N_GRAM_SIZE);
- resetWordCounter();
- } else {
- // No good n-gram at front, and buffer is full. Shift out the first word (or if there
- // is none, the existing logUnits).
- logUnits = peekAtFirstNWords(1);
- publish(logUnits, false /* canIncludePrivateData */);
- shiftOutWords(1);
+ mNumWordsUntilSafeToSample = mNumWordsBetweenNGrams;
+ return;
}
+ // No good n-gram at front, and buffer is full. Shift out up through the first logUnit
+ // with associated words (or if there is none, all the existing logUnits).
+ logUnits.clear();
+ LogUnit logUnit = shiftOut();
+ while (logUnit != null) {
+ logUnits.add(logUnit);
+ final int numWords = logUnit.getNumWords();
+ if (numWords > 0) {
+ mNumWordsUntilSafeToSample = Math.max(0, mNumWordsUntilSafeToSample - numWords);
+ break;
+ }
+ logUnit = shiftOut();
+ }
+ publish(logUnits, false /* canIncludePrivateData */);
}
/**
@@ -219,18 +231,19 @@ public abstract class MainLogBuffer extends FixedLogBuffer {
* @param logUnits The list of logUnits to be published.
* @param canIncludePrivateData Whether the private data in the logUnits can be included in
* publication.
+ *
+ * @throws IOException if publication to the log file is not possible
*/
protected abstract void publish(final ArrayList<LogUnit> logUnits,
- final boolean canIncludePrivateData);
+ final boolean canIncludePrivateData) throws IOException;
@Override
- protected void shiftOutWords(final int numWords) {
- final int oldNumActualWords = getNumActualWords();
- super.shiftOutWords(numWords);
- final int numWordsShifted = oldNumActualWords - getNumActualWords();
- mNumWordsUntilSafeToSample -= numWordsShifted;
+ protected int shiftOutWords(final int numWords) {
+ final int numWordsShiftedOut = super.shiftOutWords(numWords);
+ mNumWordsUntilSafeToSample = Math.max(0, mNumWordsUntilSafeToSample - numWordsShiftedOut);
if (DEBUG) {
Log.d(TAG, "wordsUntilSafeToSample now at " + mNumWordsUntilSafeToSample);
}
+ return numWordsShiftedOut;
}
}
diff --git a/java/src/com/android/inputmethod/research/ResearchLog.java b/java/src/com/android/inputmethod/research/ResearchLog.java
index 35a491f2c..3e82139a6 100644
--- a/java/src/com/android/inputmethod/research/ResearchLog.java
+++ b/java/src/com/android/inputmethod/research/ResearchLog.java
@@ -25,6 +25,7 @@ import com.android.inputmethod.latin.define.ProductionFlag;
import java.io.BufferedWriter;
import java.io.File;
+import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
@@ -61,7 +62,11 @@ public class ResearchLog {
/* package */ final File mFile;
private final Context mContext;
- private JsonWriter mJsonWriter = NULL_JSON_WRITER;
+ // Earlier implementations used a dummy JsonWriter that just swallowed what it was given, but
+ // this was tricky to do well, because JsonWriter throws an exception if it is passed more than
+ // one top-level object.
+ private JsonWriter mJsonWriter = null;
+
// true if at least one byte of data has been written out to the log file. This must be
// remembered because JsonWriter requires that calls matching calls to beginObject and
// endObject, as well as beginArray and endArray, and the file is opened lazily, only when
@@ -69,26 +74,6 @@ public class ResearchLog {
// could be caught, but this might suppress other errors.
private boolean mHasWrittenData = false;
- private static final JsonWriter NULL_JSON_WRITER = new JsonWriter(
- new OutputStreamWriter(new NullOutputStream()));
- private static class NullOutputStream extends OutputStream {
- /** {@inheritDoc} */
- @Override
- public void write(byte[] buffer, int offset, int count) {
- // nop
- }
-
- /** {@inheritDoc} */
- @Override
- public void write(byte[] buffer) {
- // nop
- }
-
- @Override
- public void write(int oneByte) {
- }
- }
-
public ResearchLog(final File outputFile, final Context context) {
mExecutor = Executors.newSingleThreadScheduledExecutor();
mFile = outputFile;
@@ -108,16 +93,21 @@ public class ResearchLog {
@Override
public Object call() throws Exception {
try {
- if (mHasWrittenData) {
- mJsonWriter.endArray();
- mHasWrittenData = false;
+ if (mJsonWriter == null) return null;
+ // TODO: This is necessary to avoid an exception. Better would be to not even
+ // open the JsonWriter if the file is not even opened unless there is valid data
+ // to write.
+ if (!mHasWrittenData) {
+ mJsonWriter.beginArray();
}
+ mJsonWriter.endArray();
+ mHasWrittenData = false;
mJsonWriter.flush();
mJsonWriter.close();
if (DEBUG) {
- Log.d(TAG, "wrote log to " + mFile);
+ Log.d(TAG, "closed " + mFile);
}
- } catch (Exception e) {
+ } catch (final Exception e) {
Log.d(TAG, "error when closing ResearchLog:", e);
} finally {
// Marking the file as read-only signals that this log file is ready to be
@@ -158,7 +148,14 @@ public class ResearchLog {
@Override
public Object call() throws Exception {
try {
+ if (mJsonWriter == null) return null;
if (mHasWrittenData) {
+ // TODO: This is necessary to avoid an exception. Better would be to not
+ // even open the JsonWriter if the file is not even opened unless there is
+ // valid data to write.
+ if (!mHasWrittenData) {
+ mJsonWriter.beginArray();
+ }
mJsonWriter.endArray();
mJsonWriter.close();
mHasWrittenData = false;
@@ -207,7 +204,7 @@ public class ResearchLog {
private final Callable<Object> mFlushCallable = new Callable<Object>() {
@Override
public Object call() throws Exception {
- mJsonWriter.flush();
+ if (mJsonWriter != null) mJsonWriter.flush();
return null;
}
};
@@ -253,30 +250,29 @@ public class ResearchLog {
/**
* Return a JsonWriter for this ResearchLog. It is initialized the first time this method is
* called. The cached value is returned in future calls.
+ *
+ * @throws IOException if opening the JsonWriter is not possible
*/
- public JsonWriter getInitializedJsonWriterLocked() {
- if (mJsonWriter != NULL_JSON_WRITER || mFile == null) return mJsonWriter;
+ public JsonWriter getInitializedJsonWriterLocked() throws IOException {
+ if (mJsonWriter != null) return mJsonWriter;
+ if (mFile == null) throw new FileNotFoundException();
try {
final JsonWriter jsonWriter = createJsonWriter(mContext, mFile);
- if (jsonWriter != null) {
- jsonWriter.beginArray();
- mJsonWriter = jsonWriter;
- mHasWrittenData = true;
- }
+ if (jsonWriter == null) throw new IOException("Could not create JsonWriter");
+
+ jsonWriter.beginArray();
+ mJsonWriter = jsonWriter;
+ mHasWrittenData = true;
+ return mJsonWriter;
} catch (final IOException e) {
- Log.w(TAG, "Error in JsonWriter; disabling logging", e);
- try {
- mJsonWriter.close();
- } catch (final IllegalStateException e1) {
- // Assume that this is just the json not being terminated properly.
- // Ignore
- } catch (final IOException e1) {
- Log.w(TAG, "Error in closing JsonWriter; disabling logging", e1);
- } finally {
- mJsonWriter = NULL_JSON_WRITER;
+ if (DEBUG) {
+ Log.w(TAG, "Exception when creating JsonWriter", e);
+ Log.w(TAG, "Closing JsonWriter");
}
+ if (mJsonWriter != null) mJsonWriter.close();
+ mJsonWriter = null;
+ throw e;
}
- return mJsonWriter;
}
/**
diff --git a/java/src/com/android/inputmethod/research/ResearchLogDirectory.java b/java/src/com/android/inputmethod/research/ResearchLogDirectory.java
index 291dea5d0..d156068d6 100644
--- a/java/src/com/android/inputmethod/research/ResearchLogDirectory.java
+++ b/java/src/com/android/inputmethod/research/ResearchLogDirectory.java
@@ -97,15 +97,17 @@ public class ResearchLogDirectory {
}
}
- public File getLogFilePath(final long time) {
- return new File(mFilesDir, getUniqueFilename(LOG_FILENAME_PREFIX, time));
+ public File getLogFilePath(final long time, final long nanoTime) {
+ return new File(mFilesDir, getUniqueFilename(LOG_FILENAME_PREFIX, time, nanoTime));
}
- public File getUserRecordingFilePath(final long time) {
- return new File(mFilesDir, getUniqueFilename(USER_RECORDING_FILENAME_PREFIX, time));
+ public File getUserRecordingFilePath(final long time, final long nanoTime) {
+ return new File(mFilesDir, getUniqueFilename(USER_RECORDING_FILENAME_PREFIX, time,
+ nanoTime));
}
- private static String getUniqueFilename(final String prefix, final long time) {
- return prefix + "-" + time + FILENAME_SUFFIX;
+ private static String getUniqueFilename(final String prefix, final long time,
+ final long nanoTime) {
+ return prefix + "-" + time + "-" + nanoTime + FILENAME_SUFFIX;
}
}
diff --git a/java/src/com/android/inputmethod/research/ResearchLogger.java b/java/src/com/android/inputmethod/research/ResearchLogger.java
index 7a23ddb05..8b8ea21e9 100644
--- a/java/src/com/android/inputmethod/research/ResearchLogger.java
+++ b/java/src/com/android/inputmethod/research/ResearchLogger.java
@@ -118,7 +118,6 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
private static final boolean FEEDBACK_DIALOG_SHOULD_PRESERVE_TEXT_FIELD = false;
/* package */ static boolean sIsLogging = false;
private static final int OUTPUT_FORMAT_VERSION = 5;
- private static final String PREF_USABILITY_STUDY_MODE = "usability_study_mode";
// Whether all words should be recorded, leaving unsampled word between bigrams. Useful for
// testing.
/* package for test */ static final boolean IS_LOGGING_EVERYTHING = false
@@ -150,24 +149,18 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
private static final ResearchLogger sInstance = new ResearchLogger();
private static String sAccountType = null;
private static String sAllowedAccountDomain = null;
- /* package */ ResearchLog mMainResearchLog;
+ private ResearchLog mMainResearchLog; // always non-null after init() is called
// mFeedbackLog records all events for the session, private or not (excepting
// passwords). It is written to permanent storage only if the user explicitly commands
// the system to do so.
// LogUnits are queued in the LogBuffers and published to the ResearchLogs when words are
// complete.
- /* package */ MainLogBuffer mMainLogBuffer;
- // TODO: Remove the feedback log. The feedback log continuously captured user data in case the
- // user wanted to submit it. We now use the mUserRecordingLogBuffer to allow the user to
- // explicitly reproduce a problem.
- /* package */ ResearchLog mFeedbackLog;
- /* package */ LogBuffer mFeedbackLogBuffer;
+ /* package for test */ MainLogBuffer mMainLogBuffer; // always non-null after init() is called
/* package */ ResearchLog mUserRecordingLog;
/* package */ LogBuffer mUserRecordingLogBuffer;
private File mUserRecordingFile = null;
private boolean mIsPasswordView = false;
- private boolean mIsLoggingSuspended = false;
private SharedPreferences mPrefs;
// digits entered by the user are replaced with this codepoint.
@@ -202,15 +195,6 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
private long mSavedDownEventTime;
private Bundle mFeedbackDialogBundle = null;
private boolean mInFeedbackDialog = false;
- // The feedback dialog causes stop() to be called for the keyboard connected to the original
- // window. This is because the feedback dialog must present its own EditText box that displays
- // a keyboard. stop() normally causes mFeedbackLogBuffer, which contains the user's data, to be
- // cleared, and causes mFeedbackLog, which is ready to collect information in case the user
- // wants to upload, to be closed. This is good because we don't need to log information about
- // what the user is typing in the feedback dialog, but bad because this data must be uploaded.
- // Here we save the LogBuffer and Log so the feedback dialog can later access their data.
- private LogBuffer mSavedFeedbackLogBuffer;
- private ResearchLog mSavedFeedbackLog;
private Handler mUserRecordingTimeoutHandler;
private static final long USER_RECORDING_TIMEOUT_MS = 30L * DateUtils.SECOND_IN_MILLIS;
@@ -241,6 +225,9 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
mResearchLogDirectory = new ResearchLogDirectory(mLatinIME);
cleanLogDirectoryIfNeeded(mResearchLogDirectory, System.currentTimeMillis());
+ // Initialize log buffers
+ resetLogBuffers();
+
// Initialize external services
mUploadIntent = new Intent(mLatinIME, UploaderService.class);
mUploadNowIntent = new Intent(mLatinIME, UploaderService.class);
@@ -252,6 +239,35 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
mReplayer.setKeyboardSwitcher(keyboardSwitcher);
}
+ private void resetLogBuffers() {
+ mMainResearchLog = new ResearchLog(mResearchLogDirectory.getLogFilePath(
+ System.currentTimeMillis(), System.nanoTime()), mLatinIME);
+ final int numWordsToIgnore = new Random().nextInt(NUMBER_OF_WORDS_BETWEEN_SAMPLES + 1);
+ mMainLogBuffer = new MainLogBuffer(NUMBER_OF_WORDS_BETWEEN_SAMPLES, numWordsToIgnore,
+ mSuggest) {
+ @Override
+ protected void publish(final ArrayList<LogUnit> logUnits,
+ boolean canIncludePrivateData) {
+ canIncludePrivateData |= IS_LOGGING_EVERYTHING;
+ for (final LogUnit logUnit : logUnits) {
+ if (DEBUG) {
+ final String wordsString = logUnit.getWordsAsString();
+ Log.d(TAG, "onPublish: '" + wordsString
+ + "', hc: " + logUnit.containsCorrection()
+ + ", cipd: " + canIncludePrivateData);
+ }
+ for (final String word : logUnit.getWordsAsStringArray()) {
+ final Dictionary dictionary = getDictionary();
+ mStatistics.recordWordEntered(
+ dictionary != null && dictionary.isValidWord(word),
+ logUnit.containsCorrection());
+ }
+ }
+ publishLogUnits(logUnits, mMainResearchLog, canIncludePrivateData);
+ }
+ };
+ }
+
private void cleanLogDirectoryIfNeeded(final ResearchLogDirectory researchLogDirectory,
final long now) {
final long lastCleanupTime = ResearchSettings.readResearchLastDirCleanupTime(mPrefs);
@@ -376,52 +392,9 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
Log.d(TAG, "start called");
}
maybeShowSplashScreen();
- updateSuspendedState();
requestIndicatorRedraw();
mStatistics.reset();
checkForEmptyEditor();
- if (mFeedbackLogBuffer == null) {
- resetFeedbackLogging();
- }
- if (!isAllowedToLog()) {
- // Log.w(TAG, "not in usability mode; not logging");
- return;
- }
- if (mMainLogBuffer == null) {
- mMainResearchLog = new ResearchLog(mResearchLogDirectory.getLogFilePath(
- System.currentTimeMillis()), mLatinIME);
- final int numWordsToIgnore = new Random().nextInt(NUMBER_OF_WORDS_BETWEEN_SAMPLES + 1);
- mMainLogBuffer = new MainLogBuffer(NUMBER_OF_WORDS_BETWEEN_SAMPLES, numWordsToIgnore,
- mSuggest) {
- @Override
- protected void publish(final ArrayList<LogUnit> logUnits,
- boolean canIncludePrivateData) {
- canIncludePrivateData |= IS_LOGGING_EVERYTHING;
- final int length = logUnits.size();
- for (int i = 0; i < length; i++) {
- final LogUnit logUnit = logUnits.get(i);
- final String word = logUnit.getWord();
- if (word != null && word.length() > 0 && hasLetters(word)) {
- Log.d(TAG, "onPublish: " + word + ", hc: "
- + logUnit.containsCorrection());
- final Dictionary dictionary = getDictionary();
- mStatistics.recordWordEntered(
- dictionary != null && dictionary.isValidWord(word),
- logUnit.containsCorrection());
- }
- }
- if (mMainResearchLog != null) {
- publishLogUnits(logUnits, mMainResearchLog, canIncludePrivateData);
- }
- }
- };
- }
- }
-
- private void resetFeedbackLogging() {
- mFeedbackLog = new ResearchLog(mResearchLogDirectory.getLogFilePath(
- System.currentTimeMillis()), mLatinIME);
- mFeedbackLogBuffer = new FixedLogBuffer(FEEDBACK_WORD_BUFFER_SIZE);
}
/* package */ void stop() {
@@ -431,35 +404,32 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
// Commit mCurrentLogUnit before closing.
commitCurrentLogUnit();
- if (mMainLogBuffer != null) {
- mMainLogBuffer.shiftAndPublishAll();
- logStatistics();
- commitCurrentLogUnit();
- mMainLogBuffer.setIsStopping();
+ try {
mMainLogBuffer.shiftAndPublishAll();
- mMainResearchLog.blockingClose(RESEARCHLOG_CLOSE_TIMEOUT_IN_MS);
- mMainLogBuffer = null;
+ } catch (final IOException e) {
+ Log.w(TAG, "IOException when publishing LogBuffer", e);
}
- if (mFeedbackLogBuffer != null) {
- mFeedbackLog.blockingClose(RESEARCHLOG_CLOSE_TIMEOUT_IN_MS);
- mFeedbackLogBuffer = null;
+ logStatistics();
+ commitCurrentLogUnit();
+ mMainLogBuffer.setIsStopping();
+ try {
+ mMainLogBuffer.shiftAndPublishAll();
+ } catch (final IOException e) {
+ Log.w(TAG, "IOException when publishing LogBuffer", e);
}
+ mMainResearchLog.blockingClose(RESEARCHLOG_CLOSE_TIMEOUT_IN_MS);
+
+ resetLogBuffers();
}
public void abort() {
if (DEBUG) {
Log.d(TAG, "abort called");
}
- if (mMainLogBuffer != null) {
- mMainLogBuffer.clear();
- mMainResearchLog.blockingAbort(RESEARCHLOG_ABORT_TIMEOUT_IN_MS);
- mMainLogBuffer = null;
- }
- if (mFeedbackLogBuffer != null) {
- mFeedbackLogBuffer.clear();
- mFeedbackLog.blockingAbort(RESEARCHLOG_ABORT_TIMEOUT_IN_MS);
- mFeedbackLogBuffer = null;
- }
+ mMainLogBuffer.clear();
+ mMainResearchLog.blockingAbort(RESEARCHLOG_ABORT_TIMEOUT_IN_MS);
+
+ resetLogBuffers();
}
private void restart() {
@@ -467,23 +437,11 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
start();
}
- private long mResumeTime = 0L;
- private void updateSuspendedState() {
- final long time = System.currentTimeMillis();
- if (time > mResumeTime) {
- mIsLoggingSuspended = false;
- }
- }
-
@Override
public void onSharedPreferenceChanged(final SharedPreferences prefs, final String key) {
if (key == null || prefs == null) {
return;
}
- sIsLogging = prefs.getBoolean(PREF_USABILITY_STUDY_MODE, false);
- if (sIsLogging == false) {
- abort();
- }
requestIndicatorRedraw();
mPrefs = prefs;
prefsChanged(prefs);
@@ -503,12 +461,6 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
saveRecording();
}
mInFeedbackDialog = true;
- mSavedFeedbackLogBuffer = mFeedbackLogBuffer;
- mSavedFeedbackLog = mFeedbackLog;
- // Set the non-saved versions to null so that the stop() caused by switching to the
- // Feedback dialog will not close them.
- mFeedbackLogBuffer = null;
- mFeedbackLog = null;
final Intent intent = new Intent();
intent.setClass(mLatinIME, FeedbackActivity.class);
@@ -545,7 +497,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
mUserRecordingLog.blockingAbort(RESEARCHLOG_ABORT_TIMEOUT_IN_MS);
}
mUserRecordingFile = mResearchLogDirectory.getUserRecordingFilePath(
- System.currentTimeMillis());
+ System.currentTimeMillis(), System.nanoTime());
mUserRecordingLog = new ResearchLog(mUserRecordingFile, mLatinIME);
mUserRecordingLogBuffer = new LogBuffer();
resetRecordingTimer();
@@ -666,12 +618,6 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
new LogStatement("UserFeedback", false, false, "contents", "accountName", "recording");
public void sendFeedback(final String feedbackContents, final boolean includeHistory,
final boolean isIncludingAccountName, final boolean isIncludingRecording) {
- if (mSavedFeedbackLogBuffer == null) {
- return;
- }
- if (!includeHistory) {
- mSavedFeedbackLogBuffer.clear();
- }
String recording = "";
if (isIncludingRecording) {
// Try to read recording from recently written json file
@@ -703,9 +649,13 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
final String accountName = isIncludingAccountName ? getAccountName() : "";
feedbackLogUnit.addLogStatement(LOGSTATEMENT_FEEDBACK, SystemClock.uptimeMillis(),
feedbackContents, accountName, recording);
- mFeedbackLogBuffer.shiftIn(feedbackLogUnit);
- publishLogBuffer(mFeedbackLogBuffer, mSavedFeedbackLog, true /* isIncludingPrivateData */);
- mSavedFeedbackLog.blockingClose(RESEARCHLOG_CLOSE_TIMEOUT_IN_MS);
+
+ final ResearchLog feedbackLog = new ResearchLog(mResearchLogDirectory.getLogFilePath(
+ System.currentTimeMillis(), System.nanoTime()), mLatinIME);
+ final LogBuffer feedbackLogBuffer = new LogBuffer();
+ feedbackLogBuffer.shiftIn(feedbackLogUnit);
+ publishLogBuffer(feedbackLogBuffer, feedbackLog, true /* isIncludingPrivateData */);
+ feedbackLog.blockingClose(RESEARCHLOG_CLOSE_TIMEOUT_IN_MS);
uploadNow();
if (isIncludingRecording && DEBUG_REPLAY_AFTER_FEEDBACK) {
@@ -744,8 +694,8 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
public void initSuggest(final Suggest suggest) {
mSuggest = suggest;
- // MainLogBuffer has out-of-date Suggest object. Need to close it down and create a new
- // one.
+ // MainLogBuffer now has an out-of-date Suggest object. Close down MainLogBuffer and create
+ // a new one.
if (mMainLogBuffer != null) {
stop();
start();
@@ -764,7 +714,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
}
private boolean isAllowedToLog() {
- return !mIsPasswordView && !mIsLoggingSuspended && sIsLogging && !mInFeedbackDialog;
+ return !mIsPasswordView && sIsLogging && !mInFeedbackDialog;
}
public void requestIndicatorRedraw() {
@@ -813,7 +763,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
// enabled. The dot is actually a zero-width, zero-height rectangle, placed at the
// lower-right corner of the canvas, painted with a non-zero border width.
paint.setStrokeWidth(3);
- canvas.drawRect(width, height, width, height, paint);
+ canvas.drawRect(width - 1, height - 1, width, height, paint);
}
paint.setColor(savedColor);
paint.setStyle(savedStyle);
@@ -852,16 +802,11 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
/* package for test */ void commitCurrentLogUnit() {
if (DEBUG) {
- Log.d(TAG, "commitCurrentLogUnit" + (mCurrentLogUnit.hasWord() ?
- ": " + mCurrentLogUnit.getWord() : ""));
+ Log.d(TAG, "commitCurrentLogUnit" + (mCurrentLogUnit.hasOneOrMoreWords() ?
+ ": " + mCurrentLogUnit.getWordsAsString() : ""));
}
if (!mCurrentLogUnit.isEmpty()) {
- if (mMainLogBuffer != null) {
- mMainLogBuffer.shiftIn(mCurrentLogUnit);
- }
- if (mFeedbackLogBuffer != null) {
- mFeedbackLogBuffer.shiftIn(mCurrentLogUnit);
- }
+ mMainLogBuffer.shiftIn(mCurrentLogUnit);
if (mUserRecordingLogBuffer != null) {
mUserRecordingLogBuffer.shiftIn(mCurrentLogUnit);
}
@@ -886,15 +831,12 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
//
// 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.
- if (mMainLogBuffer == null) {
- return;
- }
final LogUnit oldLogUnit = mMainLogBuffer.peekLastLogUnit();
// Check that expected word matches.
if (oldLogUnit != null) {
- final String oldLogUnitWord = oldLogUnit.getWord();
- if (!oldLogUnitWord.equals(expectedWord)) {
+ final String oldLogUnitWords = oldLogUnit.getWordsAsString();
+ if (oldLogUnitWords != null && !oldLogUnitWords.equals(expectedWord)) {
return;
}
}
@@ -910,13 +852,11 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
} else {
mCurrentLogUnit = oldLogUnit;
}
- if (mFeedbackLogBuffer != null) {
- mFeedbackLogBuffer.unshiftIn();
- }
enqueueEvent(LOGSTATEMENT_UNCOMMIT_CURRENT_LOGUNIT);
if (DEBUG) {
Log.d(TAG, "uncommitCurrentLogUnit (dump=" + dumpCurrentLogUnit + ") back to "
- + (mCurrentLogUnit.hasWord() ? ": '" + mCurrentLogUnit.getWord() + "'" : ""));
+ + (mCurrentLogUnit.hasOneOrMoreWords() ? ": '"
+ + mCurrentLogUnit.getWordsAsString() + "'" : ""));
}
}
@@ -941,6 +881,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;
// LogUnits not containing private data, such as contextual data for the log, do not require
// logSegment boundary statements.
if (canIncludePrivateData) {
@@ -950,8 +891,9 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
}
for (LogUnit logUnit : logUnits) {
if (DEBUG) {
- Log.d(TAG, "publishLogBuffer: " + (logUnit.hasWord() ? logUnit.getWord()
- : "<wordless>") + ", correction?: " + logUnit.containsCorrection());
+ Log.d(TAG, "publishLogBuffer: " + (logUnit.hasOneOrMoreWords()
+ ? logUnit.getWordsAsString() : "<wordless>")
+ + ", correction?: " + logUnit.containsCorrection());
}
researchLog.publish(logUnit, canIncludePrivateData);
}
@@ -986,7 +928,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
return;
}
if (word.length() > 0 && hasLetters(word)) {
- mCurrentLogUnit.setWord(word);
+ mCurrentLogUnit.setWords(word);
}
final LogUnit newLogUnit = mCurrentLogUnit.splitByTime(maxTime);
enqueueCommitText(word, isBatchMode);
@@ -1107,7 +1049,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
packageInfo = mLatinIME.getPackageManager().getPackageInfo(mLatinIME.getPackageName(),
0);
final String versionName = packageInfo.versionName;
- return !(developerBuildRegex.matcher(versionName).find());
+ return developerBuildRegex.matcher(versionName).find();
} catch (final NameNotFoundException e) {
Log.e(TAG, "Could not determine package name", e);
return false;
@@ -1373,11 +1315,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
public static void latinIME_promotePhantomSpace() {
final ResearchLogger researchLogger = getInstance();
final LogUnit logUnit;
- if (researchLogger.mMainLogBuffer == null) {
- logUnit = researchLogger.mCurrentLogUnit;
- } else {
- logUnit = researchLogger.mMainLogBuffer.peekLastLogUnit();
- }
+ logUnit = researchLogger.mMainLogBuffer.peekLastLogUnit();
researchLogger.enqueueEvent(logUnit, LOGSTATEMENT_LATINIME_PROMOTEPHANTOMSPACE);
}
@@ -1394,11 +1332,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
final String charactersAfterSwap) {
final ResearchLogger researchLogger = getInstance();
final LogUnit logUnit;
- if (researchLogger.mMainLogBuffer == null) {
- logUnit = null;
- } else {
- logUnit = researchLogger.mMainLogBuffer.peekLastLogUnit();
- }
+ logUnit = researchLogger.mMainLogBuffer.peekLastLogUnit();
if (logUnit != null) {
researchLogger.enqueueEvent(logUnit, LOGSTATEMENT_LATINIME_SWAPSWAPPERANDSPACE,
originalCharacters, charactersAfterSwap);
@@ -1471,14 +1405,10 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
final ResearchLogger researchLogger = getInstance();
// TODO: Verify that mCurrentLogUnit has been restored and contains the reverted word.
final LogUnit logUnit;
- if (researchLogger.mMainLogBuffer == null) {
- logUnit = null;
- } else {
- logUnit = researchLogger.mMainLogBuffer.peekLastLogUnit();
- }
+ logUnit = researchLogger.mMainLogBuffer.peekLastLogUnit();
if (originallyTypedWord.length() > 0 && hasLetters(originallyTypedWord)) {
if (logUnit != null) {
- logUnit.setWord(originallyTypedWord);
+ logUnit.setWords(originallyTypedWord);
}
}
researchLogger.enqueueEvent(logUnit != null ? logUnit : researchLogger.mCurrentLogUnit,
@@ -1616,7 +1546,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
* Log a call to LatinIME.commitCurrentAutoCorrection().
*
* SystemResponse: The IME has committed an auto-correction. An auto-correction changes the raw
- * text input to another word that the user more likely desired to type.
+ * text input to another word (or words) that the user more likely desired to type.
*/
private static final LogStatement LOGSTATEMENT_LATINIME_COMMITCURRENTAUTOCORRECTION =
new LogStatement("LatinIMECommitCurrentAutoCorrection", true, true, "typedWord",
@@ -1826,6 +1756,9 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
public static void latinIME_onEndBatchInput(final CharSequence enteredText,
final int enteredWordPos, final SuggestedWords suggestedWords) {
final ResearchLogger researchLogger = getInstance();
+ if (!TextUtils.isEmpty(enteredText) && hasLetters(enteredText.toString())) {
+ researchLogger.mCurrentLogUnit.setWords(enteredText.toString());
+ }
researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_ONENDBATCHINPUT, enteredText,
enteredWordPos);
researchLogger.mCurrentLogUnit.initializeSuggestions(suggestedWords);
diff --git a/java/src/com/android/inputmethod/research/UploaderService.java b/java/src/com/android/inputmethod/research/UploaderService.java
index 6a9717b7c..d2db34927 100644
--- a/java/src/com/android/inputmethod/research/UploaderService.java
+++ b/java/src/com/android/inputmethod/research/UploaderService.java
@@ -22,6 +22,7 @@ import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
+import android.os.SystemClock;
import com.android.inputmethod.latin.define.ProductionFlag;
@@ -79,28 +80,14 @@ public final class UploaderService extends IntentService {
*/
public static void cancelAndRescheduleUploadingService(final Context context,
final boolean needsRescheduling) {
- final PendingIntent pendingIntent = getPendingIntentForService(context);
+ final Intent intent = new Intent(context, UploaderService.class);
+ final PendingIntent pendingIntent = PendingIntent.getService(context, 0, intent, 0);
final AlarmManager alarmManager = (AlarmManager) context.getSystemService(
Context.ALARM_SERVICE);
- cancelAnyScheduledServiceAlarm(alarmManager, pendingIntent);
+ alarmManager.cancel(pendingIntent);
if (needsRescheduling) {
- scheduleServiceAlarm(alarmManager, pendingIntent);
+ alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime()
+ + UploaderService.RUN_INTERVAL, pendingIntent);
}
}
-
- private static PendingIntent getPendingIntentForService(final Context context) {
- final Intent intent = new Intent(context, UploaderService.class);
- return PendingIntent.getService(context, 0, intent, 0);
- }
-
- private static void cancelAnyScheduledServiceAlarm(final AlarmManager alarmManager,
- final PendingIntent pendingIntent) {
- alarmManager.cancel(pendingIntent);
- }
-
- private static void scheduleServiceAlarm(final AlarmManager alarmManager,
- final PendingIntent pendingIntent) {
- alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, UploaderService.RUN_INTERVAL,
- pendingIntent);
- }
}