diff options
Diffstat (limited to 'java/src/com/android/inputmethod')
34 files changed, 748 insertions, 262 deletions
diff --git a/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardViewProxy.java b/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardViewProxy.java index c0028e4cf..73896dfd3 100644 --- a/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardViewProxy.java +++ b/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardViewProxy.java @@ -64,6 +64,9 @@ public final class AccessibleKeyboardViewProxy extends AccessibilityDelegateComp */ private int mEdgeSlop; + /** The most recently set keyboard mode. */ + private int mLastKeyboardMode; + public static void init(final InputMethodService inputMethod) { sInstance.initInternal(inputMethod); } @@ -113,16 +116,19 @@ public final class AccessibleKeyboardViewProxy extends AccessibilityDelegateComp if (mView == null) { return; } - if (mAccessibilityNodeProvider != null) { mAccessibilityNodeProvider.setKeyboard(); } + final int keyboardMode = mView.getKeyboard().mId.mMode; // Since this method is called even when accessibility is off, make sure - // to check the state before announcing anything. - if (AccessibilityUtils.getInstance().isAccessibilityEnabled()) { - announceKeyboardMode(); + // to check the state before announcing anything. Also, don't announce + // changes within the same mode. + if (AccessibilityUtils.getInstance().isAccessibilityEnabled() + && (mLastKeyboardMode != keyboardMode)) { + announceKeyboardMode(keyboardMode); } + mLastKeyboardMode = keyboardMode; } /** @@ -132,25 +138,24 @@ public final class AccessibleKeyboardViewProxy extends AccessibilityDelegateComp if (mView == null) { return; } - announceKeyboardHidden(); + mLastKeyboardMode = -1; } /** * Announces which type of keyboard is being displayed. If the keyboard type * is unknown, no announcement is made. + * + * @param mode The new keyboard mode. */ - private void announceKeyboardMode() { - final Keyboard keyboard = mView.getKeyboard(); - final int resId = KEYBOARD_MODE_RES_IDS.get(keyboard.mId.mMode); + private void announceKeyboardMode(int mode) { + final int resId = KEYBOARD_MODE_RES_IDS.get(mode); if (resId == 0) { return; } - final Context context = mView.getContext(); final String keyboardMode = context.getString(resId); final String text = context.getString(R.string.announce_keyboard_mode, keyboardMode); - sendWindowStateChanged(text); } @@ -167,7 +172,7 @@ public final class AccessibleKeyboardViewProxy extends AccessibilityDelegateComp /** * Sends a window state change event with the specified text. * - * @param text + * @param text The text to send with the event. */ private void sendWindowStateChanged(final String text) { final AccessibilityEvent stateChange = AccessibilityEvent.obtain( @@ -195,7 +200,6 @@ public final class AccessibleKeyboardViewProxy extends AccessibilityDelegateComp if (mView == null) { return null; } - return getAccessibilityNodeProvider(); } @@ -248,11 +252,9 @@ public final class AccessibleKeyboardViewProxy extends AccessibilityDelegateComp case MotionEvent.ACTION_HOVER_MOVE: if (key != previousKey) { return onTransitionKey(key, previousKey, event); - } else { - return onHoverKey(key, event); } + return onHoverKey(key, event); } - return false; } @@ -294,18 +296,13 @@ public final class AccessibleKeyboardViewProxy extends AccessibilityDelegateComp private boolean onTransitionKey(final Key currentKey, final Key previousKey, final MotionEvent event) { final int savedAction = event.getAction(); - event.setAction(MotionEvent.ACTION_HOVER_EXIT); onHoverKey(previousKey, event); - event.setAction(MotionEvent.ACTION_HOVER_ENTER); onHoverKey(currentKey, event); - event.setAction(MotionEvent.ACTION_HOVER_MOVE); final boolean handled = onHoverKey(currentKey, event); - event.setAction(savedAction); - return handled; } diff --git a/java/src/com/android/inputmethod/dictionarypack/DictionaryPackConstants.java b/java/src/com/android/inputmethod/dictionarypack/DictionaryPackConstants.java index 0c8b466a4..69615887f 100644 --- a/java/src/com/android/inputmethod/dictionarypack/DictionaryPackConstants.java +++ b/java/src/com/android/inputmethod/dictionarypack/DictionaryPackConstants.java @@ -25,16 +25,34 @@ package com.android.inputmethod.dictionarypack; */ public class DictionaryPackConstants { /** + * The root domain for the dictionary pack, upon which authorities and actions will append + * their own distinctive strings. + */ + private static final String DICTIONARY_DOMAIN = "com.android.inputmethod.dictionarypack"; + + /** * Authority for the ContentProvider protocol. */ // TODO: find some way to factorize this string with the one in the resources - public static final String AUTHORITY = "com.android.inputmethod.dictionarypack.aosp"; + public static final String AUTHORITY = DICTIONARY_DOMAIN + ".aosp"; /** * The action of the intent for publishing that new dictionary data is available. */ // TODO: make this different across different packages. A suggested course of action is // to use the package name inside this string. - public static final String NEW_DICTIONARY_INTENT_ACTION = - "com.android.inputmethod.dictionarypack.newdict"; + // NOTE: The appended string should be uppercase like all other actions, but it's not for + // historical reasons. + public static final String NEW_DICTIONARY_INTENT_ACTION = DICTIONARY_DOMAIN + ".newdict"; + + /** + * The action of the intent sent by the dictionary pack to ask for a client to make + * itself known. This is used when the settings activity is brought up for a client the + * dictionary pack does not know about. + */ + public static final String UNKNOWN_DICTIONARY_PROVIDER_CLIENT = DICTIONARY_DOMAIN + + ".UNKNOWN_CLIENT"; + // In the above intents, the name of the string extra that contains the name of the client + // we want information about. + public static final String DICTIONARY_PROVIDER_CLIENT_EXTRA = "client"; } diff --git a/java/src/com/android/inputmethod/dictionarypack/DictionaryProvider.java b/java/src/com/android/inputmethod/dictionarypack/DictionaryProvider.java index 77b3b8e2e..f8d1c4fc9 100644 --- a/java/src/com/android/inputmethod/dictionarypack/DictionaryProvider.java +++ b/java/src/com/android/inputmethod/dictionarypack/DictionaryProvider.java @@ -509,6 +509,11 @@ public final class DictionaryProvider extends ContentProvider { } catch (final BadFormatException e) { Log.w(TAG, "Not enough information to insert this dictionary " + values, e); } + // We just received new information about the list of dictionary for this client. + // For all intents and purposes, this is new metadata, so we should publish it + // so that any listeners (like the Settings interface for example) can update + // themselves. + UpdateHandler.publishUpdateMetadataCompleted(getContext(), true); break; case DICTIONARY_V1_WHOLE_LIST: case DICTIONARY_V1_DICT_INFO: diff --git a/java/src/com/android/inputmethod/dictionarypack/DictionarySettingsFragment.java b/java/src/com/android/inputmethod/dictionarypack/DictionarySettingsFragment.java index 7e2a6bb1e..9e27c1f3f 100644 --- a/java/src/com/android/inputmethod/dictionarypack/DictionarySettingsFragment.java +++ b/java/src/com/android/inputmethod/dictionarypack/DictionarySettingsFragment.java @@ -110,6 +110,15 @@ public final class DictionarySettingsFragment extends PreferenceFragment super.onResume(); mChangedSettings = false; UpdateHandler.registerUpdateEventListener(this); + final Activity activity = getActivity(); + if (!MetadataDbHelper.isClientKnown(activity, mClientId)) { + Log.i(TAG, "Unknown dictionary pack client: " + mClientId + ". Requesting info."); + final Intent unknownClientBroadcast = + new Intent(DictionaryPackConstants.UNKNOWN_DICTIONARY_PROVIDER_CLIENT); + unknownClientBroadcast.putExtra( + DictionaryPackConstants.DICTIONARY_PROVIDER_CLIENT_EXTRA, mClientId); + activity.sendBroadcast(unknownClientBroadcast); + } final IntentFilter filter = new IntentFilter(); filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION); getActivity().registerReceiver(mConnectivityChangedReceiver, filter); @@ -130,6 +139,7 @@ public final class DictionarySettingsFragment extends PreferenceFragment } } + @Override public void downloadedMetadata(final boolean succeeded) { stopLoadingAnimation(); if (!succeeded) return; // If the download failed nothing changed, so no need to refresh @@ -141,6 +151,7 @@ public final class DictionarySettingsFragment extends PreferenceFragment }.start(); } + @Override public void wordListDownloadFinished(final String wordListId, final boolean succeeded) { final WordListPreference pref = findWordListPreference(wordListId); if (null == pref) return; @@ -177,6 +188,7 @@ public final class DictionarySettingsFragment extends PreferenceFragment return null; } + @Override public void updateCycleCompleted() {} private void refreshNetworkState() { @@ -260,6 +272,7 @@ public final class DictionarySettingsFragment extends PreferenceFragment } else if (!cursor.moveToFirst()) { final ArrayList<Preference> result = new ArrayList<Preference>(); result.add(createErrorMessage(activity, R.string.no_dictionaries_available)); + cursor.close(); return result; } else { final String systemLocaleString = Locale.getDefault().toString(); @@ -289,6 +302,7 @@ public final class DictionarySettingsFragment extends PreferenceFragment prefList.put(key, pref); } } while (cursor.moveToNext()); + cursor.close(); return prefList.values(); } } @@ -335,8 +349,7 @@ public final class DictionarySettingsFragment extends PreferenceFragment private void cancelRefresh() { UpdateHandler.unregisterUpdateEventListener(this); final Context context = getActivity(); - UpdateHandler.cancelUpdate(context, - MetadataDbHelper.getMetadataUriAsString(context, mClientId)); + UpdateHandler.cancelUpdate(context, mClientId); stopLoadingAnimation(); } @@ -359,7 +372,12 @@ public final class DictionarySettingsFragment extends PreferenceFragment getActivity(), android.R.anim.fade_out)); preferenceView.startAnimation(AnimationUtils.loadAnimation( getActivity(), android.R.anim.fade_in)); - mUpdateNowMenu.setTitle(R.string.check_for_updates_now); + // The menu is created by the framework asynchronously after the activity, + // which means it's possible to have the activity running but the menu not + // created yet - hence the necessity for a null check here. + if (null != mUpdateNowMenu) { + mUpdateNowMenu.setTitle(R.string.check_for_updates_now); + } } }); } diff --git a/java/src/com/android/inputmethod/dictionarypack/EventHandler.java b/java/src/com/android/inputmethod/dictionarypack/EventHandler.java index 96c4a8305..d8aa33bb8 100644 --- a/java/src/com/android/inputmethod/dictionarypack/EventHandler.java +++ b/java/src/com/android/inputmethod/dictionarypack/EventHandler.java @@ -16,13 +16,9 @@ package com.android.inputmethod.dictionarypack; -import com.android.inputmethod.latin.LatinIME; -import com.android.inputmethod.latin.R; - import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; -import android.util.Log; public final class EventHandler extends BroadcastReceiver { private static final String TAG = EventHandler.class.getName(); diff --git a/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java b/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java index b4727509c..e05a79b7b 100644 --- a/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java +++ b/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java @@ -444,7 +444,19 @@ public final class UpdateHandler { manager.remove(fileId); } - private static void publishUpdateMetadataCompleted(final Context context, + /** + * Sends a broadcast informing listeners that the dictionaries were updated. + * + * This will call all local listeners through the UpdateEventListener#downloadedMetadata + * callback (for example, the dictionary provider interface uses this to stop the Loading + * animation) and send a broadcast about the metadata having been updated. For a client of + * the dictionary pack like Latin IME, this means it should re-query the dictionary pack + * for any relevant new data. + * + * @param context the context, to send the broadcast. + * @param downloadSuccessful whether the download of the metadata was successful or not. + */ + public static void publishUpdateMetadataCompleted(final Context context, final boolean downloadSuccessful) { // We need to warn all listeners of what happened. But some listeners may want to // remove themselves or re-register something in response. Hence we should take a diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java b/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java index fd9edec70..5e68c7067 100644 --- a/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java +++ b/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java @@ -301,8 +301,10 @@ public final class KeyboardLayoutSet { final int xmlId = mResources.getIdentifier(keyboardLayoutSetName, "xml", packageName); try { parseKeyboardLayoutSet(mResources, xmlId); - } catch (Exception e) { - throw new RuntimeException(e.getMessage() + " in " + keyboardLayoutSetName); + } catch (final IOException e) { + throw new RuntimeException(e.getMessage() + " in " + keyboardLayoutSetName, e); + } catch (final XmlPullParserException e) { + throw new RuntimeException(e.getMessage() + " in " + keyboardLayoutSetName, e); } return new KeyboardLayoutSet(mContext, mParams); } @@ -311,14 +313,14 @@ public final class KeyboardLayoutSet { throws XmlPullParserException, IOException { final XmlResourceParser parser = res.getXml(resId); try { - int event; - while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) { + while (parser.getEventType() != XmlPullParser.END_DOCUMENT) { + final int event = parser.next(); if (event == XmlPullParser.START_TAG) { final String tag = parser.getName(); if (TAG_KEYBOARD_SET.equals(tag)) { parseKeyboardLayoutSetContent(parser); } else { - throw new XmlParseUtils.IllegalStartTag(parser, TAG_KEYBOARD_SET); + throw new XmlParseUtils.IllegalStartTag(parser, tag, TAG_KEYBOARD_SET); } } } @@ -329,21 +331,21 @@ public final class KeyboardLayoutSet { private void parseKeyboardLayoutSetContent(final XmlPullParser parser) throws XmlPullParserException, IOException { - int event; - while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) { + while (parser.getEventType() != XmlPullParser.END_DOCUMENT) { + final int event = parser.next(); if (event == XmlPullParser.START_TAG) { final String tag = parser.getName(); if (TAG_ELEMENT.equals(tag)) { parseKeyboardLayoutSetElement(parser); } else { - throw new XmlParseUtils.IllegalStartTag(parser, TAG_KEYBOARD_SET); + throw new XmlParseUtils.IllegalStartTag(parser, tag, TAG_KEYBOARD_SET); } } else if (event == XmlPullParser.END_TAG) { final String tag = parser.getName(); if (TAG_KEYBOARD_SET.equals(tag)) { break; } else { - throw new XmlParseUtils.IllegalEndTag(parser, TAG_KEYBOARD_SET); + throw new XmlParseUtils.IllegalEndTag(parser, tag, TAG_KEYBOARD_SET); } } } diff --git a/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java b/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java index f0ca9c1ec..745e7dfed 100644 --- a/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java +++ b/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java @@ -229,6 +229,9 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack @Override public void handleMessage(final Message msg) { final MainKeyboardView keyboardView = getOuterInstance(); + if (keyboardView == null) { + return; + } final PointerTracker tracker = (PointerTracker) msg.obj; switch (msg.what) { case MSG_TYPING_STATE_EXPIRED: diff --git a/java/src/com/android/inputmethod/keyboard/internal/GesturePreviewTrail.java b/java/src/com/android/inputmethod/keyboard/internal/GesturePreviewTrail.java index b047fe038..e3e6d39e4 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/GesturePreviewTrail.java +++ b/java/src/com/android/inputmethod/keyboard/internal/GesturePreviewTrail.java @@ -44,6 +44,7 @@ final class GesturePreviewTrail { // The wall time of the zero value in {@link #mEventTimes} private long mCurrentTimeBase; private int mTrailStartIndex; + private int mLastInterpolatedDrawIndex; static final class Params { public final int mTrailColor; @@ -96,6 +97,17 @@ final class GesturePreviewTrail { } final int[] eventTimes = mEventTimes.getPrimitiveArray(); final int strokeId = stroke.getGestureStrokeId(); + // Because interpolation algorithm in {@link GestureStrokeWithPreviewPoints} can't determine + // the interpolated points in the last segment of gesture stroke, it may need recalculation + // of interpolation when new segments are added to the stroke. + // {@link #mLastInterpolatedDrawIndex} holds the start index of the last segment. It may + // be updated by the interpolation + // {@link GestureStrokeWithPreviewPoints#interpolatePreviewStroke} + // or by animation {@link #drawGestureTrail(Canvas,Paint,Rect,Params)} below. + final int lastInterpolatedIndex = (strokeId == mCurrentStrokeId) + ? mLastInterpolatedDrawIndex : trailSize; + mLastInterpolatedDrawIndex = stroke.interpolateStrokeAndReturnStartIndexOfLastSegment( + lastInterpolatedIndex, mEventTimes, mXCoordinates, mYCoordinates); if (strokeId != mCurrentStrokeId) { final int elapsedTime = (int)(downTime - mCurrentTimeBase); for (int i = mTrailStartIndex; i < trailSize; i++) { @@ -216,6 +228,10 @@ final class GesturePreviewTrail { System.arraycopy(eventTimes, startIndex, eventTimes, 0, newSize); System.arraycopy(xCoords, startIndex, xCoords, 0, newSize); System.arraycopy(yCoords, startIndex, yCoords, 0, 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. + mLastInterpolatedDrawIndex = Math.max(mLastInterpolatedDrawIndex - startIndex, 0); } mEventTimes.setLength(newSize); mXCoordinates.setLength(newSize); diff --git a/java/src/com/android/inputmethod/keyboard/internal/GestureStrokeWithPreviewPoints.java b/java/src/com/android/inputmethod/keyboard/internal/GestureStrokeWithPreviewPoints.java index fc81410ff..3315954c1 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/GestureStrokeWithPreviewPoints.java +++ b/java/src/com/android/inputmethod/keyboard/internal/GestureStrokeWithPreviewPoints.java @@ -21,19 +21,32 @@ 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 int mStrokeId; private int mLastPreviewSize; + private final HermiteInterpolator mInterpolator = new HermiteInterpolator(); + private int mLastInterpolatedPreviewIndex; - private int mMinPreviewSampleLengthSquare; + private int mMinPreviewSamplingDistanceSquared; private int mLastX; private int mLastY; + private double mMinPreviewSamplingDistance; + private double mDistanceFromLastSample; - // TODO: Move this to resource. - private static final float MIN_PREVIEW_SAMPLE_LENGTH_RATIO_TO_KEY_WIDTH = 0.1f; + // 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); @@ -44,6 +57,7 @@ public final class GestureStrokeWithPreviewPoints extends GestureStroke { super.reset(); mStrokeId++; mLastPreviewSize = 0; + mLastInterpolatedPreviewIndex = 0; mPreviewEventTimes.setLength(0); mPreviewXCoordinates.setLength(0); mPreviewYCoordinates.setLength(0); @@ -53,35 +67,49 @@ public final class GestureStrokeWithPreviewPoints extends GestureStroke { return mStrokeId; } - public int getGestureStrokePreviewSize() { - return mPreviewEventTimes.getLength(); - } - @Override public void setKeyboardGeometry(final int keyWidth, final int keyboardHeight) { super.setKeyboardGeometry(keyWidth, keyboardHeight); - final float sampleLength = keyWidth * MIN_PREVIEW_SAMPLE_LENGTH_RATIO_TO_KEY_WIDTH; - mMinPreviewSampleLengthSquare = (int)(sampleLength * sampleLength); + 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) { + 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; - return dx * dx + dy * dy >= mMinPreviewSampleLengthSquare; + if (isMajorEvent || dx * dx + dy * dy >= mMinPreviewSamplingDistanceSquared) { + mLastX = x; + mLastY = y; + return true; + } + return false; } @Override public boolean addPointOnKeyboard(final int x, final int y, final int time, final boolean isMajorEvent) { - final boolean onValidArea = super.addPointOnKeyboard(x, y, time, isMajorEvent); - if (isMajorEvent || needsSampling(x, y)) { + if (needsSampling(x, y, isMajorEvent)) { mPreviewEventTimes.add(time); mPreviewXCoordinates.add(x); mPreviewYCoordinates.add(y); - mLastX = x; - mLastY = y; } - return onValidArea; + return super.addPointOnKeyboard(x, y, time, isMajorEvent); + } public void appendPreviewStroke(final ResizableIntArray eventTimes, @@ -95,4 +123,82 @@ public final class GestureStrokeWithPreviewPoints extends GestureStroke { yCoords.append(mPreviewYCoordinates, mLastPreviewSize, length); mLastPreviewSize = mPreviewEventTimes.getLength(); } + + /** + * Calculate interpolated points between the last interpolated point and the end of the trail. + * And return the start index of the last interpolated segment of input arrays because it + * may need to recalculate the interpolated points in the segment if further segments are + * added to this stroke. + * + * @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. + * @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 int size = mPreviewEventTimes.getLength(); + final int[] pt = mPreviewEventTimes.getPrimitiveArray(); + final int[] px = mPreviewXCoordinates.getPrimitiveArray(); + final int[] py = mPreviewYCoordinates.getPrimitiveArray(); + mInterpolator.reset(px, py, 0, size); + // The last segment of gesture stroke needs to be interpolated again because the slope of + // the tangent at the last point isn't determined. + int lastInterpolatedDrawIndex = lastInterpolatedIndex; + int d1 = lastInterpolatedIndex; + for (int p2 = mLastInterpolatedPreviewIndex + 1; p2 < size; p2++) { + final int p1 = p2 - 1; + final int p0 = p1 - 1; + final int p3 = p2 + 1; + mLastInterpolatedPreviewIndex = p1; + lastInterpolatedDrawIndex = d1; + 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 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; + mInterpolator.interpolate(t); + eventTimes.add(d1, (int)(dt * t) + t1); + xCoords.add(d1, (int)mInterpolator.mInterpolatedX); + yCoords.add(d1, (int)mInterpolator.mInterpolatedY); + d1++; + } + eventTimes.add(d1, pt[p2]); + xCoords.add(d1, px[p2]); + yCoords.add(d1, py[p2]); + } + return lastInterpolatedDrawIndex; + } + + private static final double TWO_PI = Math.PI * 2.0d; + + /** + * Calculate the angular of rotation from <code>a0</code> to <code>a1</code>. + * + * @param a1 the angular to which the rotation ends. + * @param a0 the angular from which the rotation starts. + * @return the angular rotation value from a0 to a1, normalized to [-PI, +PI]. + */ + private static double angularDiff(final double a1, final double a0) { + double deltaAngle = a1 - a0; + while (deltaAngle > Math.PI) { + deltaAngle -= TWO_PI; + } + while (deltaAngle < -Math.PI) { + deltaAngle += TWO_PI; + } + return deltaAngle; + } } diff --git a/java/src/com/android/inputmethod/keyboard/internal/HermiteInterpolator.java b/java/src/com/android/inputmethod/keyboard/internal/HermiteInterpolator.java new file mode 100644 index 000000000..0ec8153f5 --- /dev/null +++ b/java/src/com/android/inputmethod/keyboard/internal/HermiteInterpolator.java @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.keyboard.internal; + +import com.android.inputmethod.annotations.UsedForTesting; + +/** + * Interpolates XY-coordinates using Cubic Hermite Curve. + */ +public final class HermiteInterpolator { + private int[] mXCoords; + private int[] mYCoords; + private int mMinPos; + private int mMaxPos; + + // Working variable to calculate interpolated value. + /** The coordinates of the start point of the interval. */ + public int mP1X, mP1Y; + /** The coordinates of the end point of the interval. */ + public int mP2X, mP2Y; + /** The slope of the tangent at the start point. */ + public float mSlope1X, mSlope1Y; + /** The slope of the tangent at the end point. */ + public float mSlope2X, mSlope2Y; + /** The interpolated coordinates. + * The return variables of {@link #interpolate(float)} to avoid instantiations. + */ + public float mInterpolatedX, mInterpolatedY; + + public HermiteInterpolator() { + // Nothing to do with here. + } + + /** + * Reset this interpolator to point XY-coordinates data. + * @param xCoords the array of x-coordinates. Valid data are in left-open interval + * <code>[minPos, maxPos)</code>. + * @param yCoords the array of y-coordinates. Valid data are in left-open interval + * <code>[minPos, maxPos)</code>. + * @param minPos the minimum index of left-open interval of valid data. + * @param maxPos the maximum index of left-open interval of valid data. + */ + @UsedForTesting + public void reset(final int[] xCoords, final int[] yCoords, final int minPos, + final int maxPos) { + mXCoords = xCoords; + mYCoords = yCoords; + mMinPos = minPos; + mMaxPos = maxPos; + } + + /** + * Set interpolation interval. + * <p> + * The start and end coordinates of the interval will be set in {@link #mP1X}, {@link #mP1Y}, + * {@link #mP2X}, and {@link #mP2Y}. The slope of the tangents at start and end points will be + * set in {@link #mSlope1X}, {@link #mSlope1Y}, {@link #mSlope2X}, and {@link #mSlope2Y}. + * + * @param p0 the index just before interpolation interval. If <code>p1</code> points the start + * of valid points, <code>p0</code> must be less than <code>minPos</code> of + * {@link #reset(int[],int[],int,int)}. + * @param p1 the start index of interpolation interval. + * @param p2 the end index of interpolation interval. + * @param p3 the index just after interpolation interval. If <code>p2</code> points the end of + * valid points, <code>p3</code> must be equal or greater than <code>maxPos</code> of + * {@link #reset(int[],int[],int,int)}. + */ + @UsedForTesting + public void setInterval(final int p0, final int p1, final int p2, final int p3) { + mP1X = mXCoords[p1]; + mP1Y = mYCoords[p1]; + mP2X = mXCoords[p2]; + mP2Y = mYCoords[p2]; + // A(ax,ay) is the vector p1->p2. + final int ax = mP2X - mP1X; + final int ay = mP2Y - mP1Y; + + // Calculate the slope of the tangent at p1. + if (p0 >= mMinPos) { + // p1 has previous valid point p0. + // The slope of the tangent is half of the vector p0->p2. + mSlope1X = (mP2X - mXCoords[p0]) / 2.0f; + mSlope1Y = (mP2Y - mYCoords[p0]) / 2.0f; + } else if (p3 < mMaxPos) { + // p1 has no previous valid point, but p2 has next valid point p3. + // B(bx,by) is the slope vector of the tangent at p2. + final float bx = (mXCoords[p3] - mP1X) / 2.0f; + final float by = (mYCoords[p3] - mP1Y) / 2.0f; + final float crossProdAB = ax * by - ay * bx; + final float dotProdAB = ax * bx + ay * by; + final float normASquare = ax * ax + ay * ay; + final float invHalfNormASquare = 1.0f / normASquare / 2.0f; + // The slope of the tangent is the mirror image of vector B to vector A. + mSlope1X = invHalfNormASquare * (dotProdAB * ax + crossProdAB * ay); + mSlope1Y = invHalfNormASquare * (dotProdAB * ay - crossProdAB * ax); + } else { + // p1 and p2 have no previous valid point. (Interval has only point p1 and p2) + mSlope1X = ax; + mSlope1Y = ay; + } + + // Calculate the slope of the tangent at p2. + if (p3 < mMaxPos) { + // p2 has next valid point p3. + // The slope of the tangent is half of the vector p1->p3. + mSlope2X = (mXCoords[p3] - mP1X) / 2.0f; + mSlope2Y = (mYCoords[p3] - mP1Y) / 2.0f; + } else if (p0 >= mMinPos) { + // p2 has no next valid point, but p1 has previous valid point p0. + // B(bx,by) is the slope vector of the tangent at p1. + final float bx = (mP2X - mXCoords[p0]) / 2.0f; + final float by = (mP2Y - mYCoords[p0]) / 2.0f; + final float crossProdAB = ax * by - ay * bx; + final float dotProdAB = ax * bx + ay * by; + final float normASquare = ax * ax + ay * ay; + final float invHalfNormASquare = 1.0f / normASquare / 2.0f; + // The slope of the tangent is the mirror image of vector B to vector A. + mSlope2X = invHalfNormASquare * (dotProdAB * ax + crossProdAB * ay); + mSlope2Y = invHalfNormASquare * (dotProdAB * ay - crossProdAB * ax); + } else { + // p1 and p2 has no previous valid point. (Interval has only point p1 and p2) + mSlope2X = ax; + mSlope2Y = ay; + } + } + + /** + * Calculate interpolation value at <code>t</code> in unit interval <code>[0,1]</code>. + * <p> + * On the unit interval [0,1], given a starting point p1 at t=0 and an ending point p2 at t=1 + * with the slope of the tangent m1 at p1 and m2 at p2, the polynomial of cubic Hermite curve + * can be defined by + * p(t) = (1+2t)(1-t)(1-t)*p1 + t(1-t)(1-t)*m1 + (3-2t)t^2*p2 + (t-1)t^2*m2 + * where t is an element of [0,1]. + * <p> + * The interpolated XY-coordinates will be set in {@link #mInterpolatedX} and + * {@link #mInterpolatedY}. + * + * @param t the interpolation parameter. The value must be in close interval <code>[0,1]</code>. + */ + @UsedForTesting + public void interpolate(final float t) { + final float omt = 1.0f - t; + final float tm2 = 2.0f * t; + final float k1 = 1.0f + tm2; + final float k2 = 3.0f - tm2; + final float omt2 = omt * omt; + final float t2 = t * t; + mInterpolatedX = (k1 * mP1X + t * mSlope1X) * omt2 + (k2 * mP2X - omt * mSlope2X) * t2; + mInterpolatedY = (k1 * mP1Y + t * mSlope1Y) * omt2 + (k2 * mP2Y - omt * mSlope2Y) * t2; + } +} diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java index 8ae1b8881..be178f516 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java +++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java @@ -164,10 +164,10 @@ public class KeyboardBuilder<KP extends KeyboardParams> { parseKeyboard(parser); } catch (XmlPullParserException e) { Log.w(BUILDER_TAG, "keyboard XML parse error", e); - throw new IllegalArgumentException(e); + throw new IllegalArgumentException(e.getMessage(), e); } catch (IOException e) { Log.w(BUILDER_TAG, "keyboard XML parse error", e); - throw new RuntimeException(e); + throw new RuntimeException(e.getMessage(), e); } finally { parser.close(); } @@ -210,8 +210,8 @@ public class KeyboardBuilder<KP extends KeyboardParams> { private void parseKeyboard(final XmlPullParser parser) throws XmlPullParserException, IOException { if (DEBUG) startTag("<%s> %s", TAG_KEYBOARD, mParams.mId); - int event; - while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) { + while (parser.getEventType() != XmlPullParser.END_DOCUMENT) { + final int event = parser.next(); if (event == XmlPullParser.START_TAG) { final String tag = parser.getName(); if (TAG_KEYBOARD.equals(tag)) { @@ -220,7 +220,7 @@ public class KeyboardBuilder<KP extends KeyboardParams> { parseKeyboardContent(parser, false); break; } else { - throw new XmlParseUtils.IllegalStartTag(parser, TAG_KEYBOARD); + throw new XmlParseUtils.IllegalStartTag(parser, tag, TAG_KEYBOARD); } } } @@ -303,8 +303,8 @@ public class KeyboardBuilder<KP extends KeyboardParams> { private void parseKeyboardContent(final XmlPullParser parser, final boolean skip) throws XmlPullParserException, IOException { - int event; - while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) { + while (parser.getEventType() != XmlPullParser.END_DOCUMENT) { + final int event = parser.next(); if (event == XmlPullParser.START_TAG) { final String tag = parser.getName(); if (TAG_ROW.equals(tag)) { @@ -321,7 +321,7 @@ public class KeyboardBuilder<KP extends KeyboardParams> { } else if (TAG_KEY_STYLE.equals(tag)) { parseKeyStyle(parser, skip); } else { - throw new XmlParseUtils.IllegalStartTag(parser, TAG_ROW); + throw new XmlParseUtils.IllegalStartTag(parser, tag, TAG_ROW); } } else if (event == XmlPullParser.END_TAG) { final String tag = parser.getName(); @@ -333,7 +333,7 @@ public class KeyboardBuilder<KP extends KeyboardParams> { || TAG_MERGE.equals(tag)) { break; } else { - throw new XmlParseUtils.IllegalEndTag(parser, TAG_ROW); + throw new XmlParseUtils.IllegalEndTag(parser, tag, TAG_ROW); } } } @@ -345,10 +345,10 @@ public class KeyboardBuilder<KP extends KeyboardParams> { R.styleable.Keyboard); try { if (a.hasValue(R.styleable.Keyboard_horizontalGap)) { - throw new XmlParseUtils.IllegalAttribute(parser, "horizontalGap"); + throw new XmlParseUtils.IllegalAttribute(parser, TAG_ROW, "horizontalGap"); } if (a.hasValue(R.styleable.Keyboard_verticalGap)) { - throw new XmlParseUtils.IllegalAttribute(parser, "verticalGap"); + throw new XmlParseUtils.IllegalAttribute(parser, TAG_ROW, "verticalGap"); } return new KeyboardRow(mResources, mParams, parser, mCurrentY); } finally { @@ -358,8 +358,8 @@ public class KeyboardBuilder<KP extends KeyboardParams> { private void parseRowContent(final XmlPullParser parser, final KeyboardRow row, final boolean skip) throws XmlPullParserException, IOException { - int event; - while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) { + while (parser.getEventType() != XmlPullParser.END_DOCUMENT) { + final int event = parser.next(); if (event == XmlPullParser.START_TAG) { final String tag = parser.getName(); if (TAG_KEY.equals(tag)) { @@ -373,7 +373,7 @@ public class KeyboardBuilder<KP extends KeyboardParams> { } else if (TAG_KEY_STYLE.equals(tag)) { parseKeyStyle(parser, skip); } else { - throw new XmlParseUtils.IllegalStartTag(parser, TAG_KEY); + throw new XmlParseUtils.IllegalStartTag(parser, tag, TAG_ROW); } } else if (event == XmlPullParser.END_TAG) { final String tag = parser.getName(); @@ -387,7 +387,7 @@ public class KeyboardBuilder<KP extends KeyboardParams> { || TAG_MERGE.equals(tag)) { break; } else { - throw new XmlParseUtils.IllegalEndTag(parser, TAG_KEY); + throw new XmlParseUtils.IllegalEndTag(parser, tag, TAG_ROW); } } } @@ -506,8 +506,8 @@ public class KeyboardBuilder<KP extends KeyboardParams> { private void parseMerge(final XmlPullParser parser, final KeyboardRow row, final boolean skip) throws XmlPullParserException, IOException { if (DEBUG) startTag("<%s>", TAG_MERGE); - int event; - while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) { + while (parser.getEventType() != XmlPullParser.END_DOCUMENT) { + final int event = parser.next(); if (event == XmlPullParser.START_TAG) { final String tag = parser.getName(); if (TAG_MERGE.equals(tag)) { @@ -539,8 +539,8 @@ public class KeyboardBuilder<KP extends KeyboardParams> { final boolean skip) throws XmlPullParserException, IOException { if (DEBUG) startTag("<%s> %s", TAG_SWITCH, mParams.mId); boolean selected = false; - int event; - while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) { + while (parser.getEventType() != XmlPullParser.END_DOCUMENT) { + final int event = parser.next(); if (event == XmlPullParser.START_TAG) { final String tag = parser.getName(); if (TAG_CASE.equals(tag)) { @@ -548,7 +548,7 @@ public class KeyboardBuilder<KP extends KeyboardParams> { } else if (TAG_DEFAULT.equals(tag)) { selected |= parseDefault(parser, row, selected ? true : skip); } else { - throw new XmlParseUtils.IllegalStartTag(parser, TAG_KEY); + throw new XmlParseUtils.IllegalStartTag(parser, tag, TAG_SWITCH); } } else if (event == XmlPullParser.END_TAG) { final String tag = parser.getName(); @@ -556,7 +556,7 @@ public class KeyboardBuilder<KP extends KeyboardParams> { if (DEBUG) endTag("</%s>", TAG_SWITCH); break; } else { - throw new XmlParseUtils.IllegalEndTag(parser, TAG_KEY); + throw new XmlParseUtils.IllegalEndTag(parser, tag, TAG_SWITCH); } } } diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java index d0b382e35..7ec1c9406 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java +++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java @@ -21,7 +21,6 @@ import android.content.res.Resources; import com.android.inputmethod.annotations.UsedForTesting; import com.android.inputmethod.latin.CollectionUtils; -import com.android.inputmethod.latin.R; import java.util.HashMap; @@ -61,13 +60,14 @@ public final class KeyboardTextsSet { } } - public void loadStringResources(Context context) { - loadStringResourcesInternal(context, RESOURCE_NAMES, R.string.english_ime_name); + public void loadStringResources(final Context context) { + final int referenceId = context.getApplicationInfo().labelRes; + loadStringResourcesInternal(context, RESOURCE_NAMES, referenceId); } @UsedForTesting - void loadStringResourcesInternal(Context context, final String[] resourceNames, - int referenceId) { + void loadStringResourcesInternal(final Context context, final String[] resourceNames, + final int referenceId) { final Resources res = context.getResources(); final String packageName = res.getResourcePackageName(referenceId); for (final String resName : resourceNames) { diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java b/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java index 4bec99c04..562e1d0b7 100644 --- a/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java +++ b/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java @@ -450,4 +450,25 @@ public final class BinaryDictionaryFileDumper { info.toContentValues()); } } + + /** + * Initialize a client record with the dictionary content provider. + * + * This merely acquires the content provider and calls + * #reinitializeClientRecordInDictionaryContentProvider. + * + * @param context the context for resources and providers. + * @param clientId the client ID to use. + */ + public static void initializeClientRecordHelper(final Context context, + final String clientId) { + try { + final ContentProviderClient client = context.getContentResolver(). + acquireContentProviderClient(getProviderUriBuilder("").build()); + if (null == client) return; + reinitializeClientRecordInDictionaryContentProvider(context, client, clientId); + } catch (RemoteException e) { + Log.e(TAG, "Cannot contact the dictionary content provider", e); + } + } } diff --git a/java/src/com/android/inputmethod/latin/DictionaryPackInstallBroadcastReceiver.java b/java/src/com/android/inputmethod/latin/DictionaryPackInstallBroadcastReceiver.java index 35f3119ea..41fcb83e6 100644 --- a/java/src/com/android/inputmethod/latin/DictionaryPackInstallBroadcastReceiver.java +++ b/java/src/com/android/inputmethod/latin/DictionaryPackInstallBroadcastReceiver.java @@ -25,14 +25,35 @@ import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.ProviderInfo; import android.net.Uri; +import android.util.Log; /** - * Takes action to reload the necessary data when a dictionary pack was added/removed. + * Receives broadcasts pertaining to dictionary management and takes the appropriate action. + * + * This object receives three types of broadcasts. + * - Package installed/added. When a dictionary provider application is added or removed, we + * need to query the dictionaries. + * - New dictionary broadcast. The dictionary provider broadcasts new dictionary availability. When + * this happens, we need to re-query the dictionaries. + * - Unknown client. If the dictionary provider is in urgent need of data about some client that + * it does not know, it sends this broadcast. When we receive this, we need to tell the dictionary + * provider about ourselves. This happens when the settings for the dictionary pack are accessed, + * but Latin IME never got a chance to register itself. */ public final class DictionaryPackInstallBroadcastReceiver extends BroadcastReceiver { + private static final String TAG = DictionaryPackInstallBroadcastReceiver.class.getSimpleName(); final LatinIME mService; + public DictionaryPackInstallBroadcastReceiver() { + // This empty constructor is necessary for the system to instantiate this receiver. + // This happens when the dictionary pack says it can't find a record for our client, + // which happens when the dictionary pack settings are called before the keyboard + // was ever started once. + Log.i(TAG, "Latin IME dictionary broadcast receiver instantiated from the framework."); + mService = null; + } + public DictionaryPackInstallBroadcastReceiver(final LatinIME service) { mService = service; } @@ -44,6 +65,11 @@ public final class DictionaryPackInstallBroadcastReceiver extends BroadcastRecei // We need to reread the dictionary if a new dictionary package is installed. if (action.equals(Intent.ACTION_PACKAGE_ADDED)) { + if (null == mService) { + Log.e(TAG, "Called with intent " + action + " but we don't know the service: this " + + "should never happen"); + return; + } final Uri packageUri = intent.getData(); if (null == packageUri) return; // No package name : we can't do anything final String packageName = packageUri.getSchemeSpecificPart(); @@ -71,6 +97,11 @@ public final class DictionaryPackInstallBroadcastReceiver extends BroadcastRecei return; } else if (action.equals(Intent.ACTION_PACKAGE_REMOVED) && !intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) { + if (null == mService) { + Log.e(TAG, "Called with intent " + action + " but we don't know the service: this " + + "should never happen"); + return; + } // When the dictionary package is removed, we need to reread dictionary (to use the // next-priority one, or stop using a dictionary at all if this was the only one, // since this is the user request). @@ -82,7 +113,28 @@ public final class DictionaryPackInstallBroadcastReceiver extends BroadcastRecei // read dictionary from? mService.resetSuggestMainDict(); } else if (action.equals(DictionaryPackConstants.NEW_DICTIONARY_INTENT_ACTION)) { + if (null == mService) { + Log.e(TAG, "Called with intent " + action + " but we don't know the service: this " + + "should never happen"); + return; + } mService.resetSuggestMainDict(); + } else if (action.equals(DictionaryPackConstants.UNKNOWN_DICTIONARY_PROVIDER_CLIENT)) { + if (null != mService) { + // Careful! This is returning if the service is NOT null. This is because we + // should come here instantiated by the framework in reaction to a broadcast of + // the above action, so we should gave gone through the no-args constructor. + Log.e(TAG, "Called with intent " + action + " but we have a reference to the " + + "service: this should never happen"); + return; + } + // The dictionary provider does not know about some client. We check that it's really + // us that it needs to know about, and if it's the case, we register with the provider. + final String wantedClientId = + intent.getStringExtra(DictionaryPackConstants.DICTIONARY_PROVIDER_CLIENT_EXTRA); + final String myClientId = context.getString(R.string.dictionary_pack_client_id); + if (!wantedClientId.equals(myClientId)) return; // Not for us + BinaryDictionaryFileDumper.initializeClientRecordHelper(context, myClientId); } } } diff --git a/java/src/com/android/inputmethod/latin/ExpandableDictionary.java b/java/src/com/android/inputmethod/latin/ExpandableDictionary.java index ae2ee577f..fd81d13ca 100644 --- a/java/src/com/android/inputmethod/latin/ExpandableDictionary.java +++ b/java/src/com/android/inputmethod/latin/ExpandableDictionary.java @@ -18,6 +18,7 @@ package com.android.inputmethod.latin; import android.content.Context; import android.text.TextUtils; +import android.util.Log; import com.android.inputmethod.keyboard.ProximityInfo; import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; @@ -31,6 +32,7 @@ import java.util.LinkedList; * be searched for suggestions and valid words. */ public class ExpandableDictionary extends Dictionary { + private static final String TAG = ExpandableDictionary.class.getSimpleName(); /** * The weight to give to a word if it's length is the same as the number of typed characters. */ @@ -551,8 +553,13 @@ public class ExpandableDictionary extends Dictionary { // word. We do want however to return the correct case for the right hand side. // So we want to squash the case of the left hand side, and preserve that of the right // hand side word. - Node firstWord = searchWord(mRoots, word1.toLowerCase(), 0, null); - Node secondWord = searchWord(mRoots, word2, 0, null); + final String word1Lower = word1.toLowerCase(); + if (TextUtils.isEmpty(word1Lower) || TextUtils.isEmpty(word2)) { + Log.e(TAG, "Invalid bigram pair: " + word1 + ", " + word1Lower + ", " + word2); + return frequency; + } + final Node firstWord = searchWord(mRoots, word1Lower, 0, null); + final Node secondWord = searchWord(mRoots, word2, 0, null); LinkedList<NextWord> bigrams = firstWord.mNGrams; if (bigrams == null || bigrams.size() == 0) { firstWord.mNGrams = CollectionUtils.newLinkedList(); diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java index e3650d9cc..56b1c786e 100644 --- a/java/src/com/android/inputmethod/latin/LatinIME.java +++ b/java/src/com/android/inputmethod/latin/LatinIME.java @@ -72,6 +72,7 @@ import com.android.inputmethod.keyboard.KeyboardActionListener; import com.android.inputmethod.keyboard.KeyboardId; import com.android.inputmethod.keyboard.KeyboardSwitcher; import com.android.inputmethod.keyboard.MainKeyboardView; +import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; import com.android.inputmethod.latin.Utils.Stats; import com.android.inputmethod.latin.define.ProductionFlag; import com.android.inputmethod.latin.suggestions.SuggestionStripView; @@ -156,7 +157,7 @@ public final class LatinIME extends InputMethodService implements KeyboardAction private PositionalInfoForUserDictPendingAddition mPositionalInfoForUserDictPendingAddition = null; private final WordComposer mWordComposer = new WordComposer(); - private RichInputConnection mConnection = new RichInputConnection(this); + private final RichInputConnection mConnection = new RichInputConnection(this); // Keep track of the last selection range to decide if we need to show word alternatives private static final int NOT_A_CURSOR_POSITION = -1; @@ -803,10 +804,6 @@ public final class LatinIME extends InputMethodService implements KeyboardAction @Override public void onWindowHidden() { - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - ResearchLogger.latinIME_onWindowHidden(mLastSelectionStart, mLastSelectionEnd, - getCurrentInputConnection()); - } super.onWindowHidden(); final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); if (mainKeyboardView != null) { @@ -834,8 +831,10 @@ public final class LatinIME extends InputMethodService implements KeyboardAction // Remove pending messages related to update suggestions mHandler.cancelUpdateSuggestionStrip(); resetComposingState(true /* alsoResetLastComposedWord */); + // Notify ResearchLogger if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - ResearchLogger.getInstance().latinIME_onFinishInputViewInternal(); + ResearchLogger.latinIME_onFinishInputViewInternal(finishingInput, mLastSelectionStart, + mLastSelectionEnd, getCurrentInputConnection()); } } @@ -1145,11 +1144,11 @@ public final class LatinIME extends InputMethodService implements KeyboardAction if (!mWordComposer.isComposingWord()) return; final String typedWord = mWordComposer.getTypedWord(); if (typedWord.length() > 0) { - commitChosenWord(typedWord, LastComposedWord.COMMIT_TYPE_USER_TYPED_WORD, - separatorString); if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { ResearchLogger.getInstance().onWordFinished(typedWord, mWordComposer.isBatchMode()); } + commitChosenWord(typedWord, LastComposedWord.COMMIT_TYPE_USER_TYPED_WORD, + separatorString); } } @@ -1907,7 +1906,6 @@ public final class LatinIME extends InputMethodService implements KeyboardAction private boolean handleSeparator(final int primaryCode, final int x, final int y, final int spaceState) { if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - ResearchLogger.recordTimeForLogUnitSplit(); ResearchLogger.latinIME_handleSeparator(primaryCode, mWordComposer.isComposingWord()); } boolean didAutoCorrect = false; @@ -2176,8 +2174,9 @@ public final class LatinIME extends InputMethodService implements KeyboardAction // Called from {@link SuggestionStripView} through the {@link SuggestionStripView#Listener} // interface @Override - public void pickSuggestionManually(final int index, final String suggestion) { + public void pickSuggestionManually(final int index, final SuggestedWordInfo suggestionInfo) { final SuggestedWords suggestedWords = mSuggestedWords; + final String suggestion = suggestionInfo.mWord; // If this is a punctuation picked from the suggestion strip, pass it to onCodeInput if (suggestion.length() == 1 && isShowingPunctuationList()) { // Word separators are suggested before the user inputs something. @@ -2243,7 +2242,8 @@ public final class LatinIME extends InputMethodService implements KeyboardAction // AND it's in none of our current dictionaries (main, user or otherwise). // Please note that if mSuggest is null, it means that everything is off: suggestion // and correction, so we shouldn't try to show the hint - final boolean showingAddToDictionaryHint = index == 0 && mSuggest != null + final boolean showingAddToDictionaryHint = + SuggestedWordInfo.KIND_TYPED == suggestionInfo.mKind && mSuggest != null // If the suggestion is not in the dictionary, the hint should be shown. && !AutoCorrection.isValidWord(mSuggest.getUnigramDictionaries(), suggestion, true); @@ -2297,25 +2297,27 @@ public final class LatinIME extends InputMethodService implements KeyboardAction // expect to receive non-words. if (!mSettings.getCurrent().mCorrectionEnabled) return null; + final Suggest suggest = mSuggest; final UserHistoryDictionary userHistoryDictionary = mUserHistoryDictionary; - if (userHistoryDictionary != null) { - final String prevWord - = mConnection.getNthPreviousWord(mSettings.getCurrent().mWordSeparators, 2); - final String secondWord; - if (mWordComposer.wasAutoCapitalized() && !mWordComposer.isMostlyCaps()) { - secondWord = suggestion.toLowerCase(mSubtypeSwitcher.getCurrentSubtypeLocale()); - } else { - secondWord = suggestion; - } - // We demote unrecognized words (frequency < 0, below) by specifying them as "invalid". - // We don't add words with 0-frequency (assuming they would be profanity etc.). - final int maxFreq = AutoCorrection.getMaxFrequency( - mSuggest.getUnigramDictionaries(), suggestion); - if (maxFreq == 0) return null; - userHistoryDictionary.addToUserHistory(prevWord, secondWord, maxFreq > 0); - return prevWord; + if (suggest == null || userHistoryDictionary == null) { + // Avoid concurrent issue + return null; + } + final String prevWord + = mConnection.getNthPreviousWord(mSettings.getCurrent().mWordSeparators, 2); + final String secondWord; + if (mWordComposer.wasAutoCapitalized() && !mWordComposer.isMostlyCaps()) { + secondWord = suggestion.toLowerCase(mSubtypeSwitcher.getCurrentSubtypeLocale()); + } else { + secondWord = suggestion; } - return null; + // We demote unrecognized words (frequency < 0, below) by specifying them as "invalid". + // We don't add words with 0-frequency (assuming they would be profanity etc.). + final int maxFreq = AutoCorrection.getMaxFrequency( + suggest.getUnigramDictionaries(), suggestion); + if (maxFreq == 0) return null; + userHistoryDictionary.addToUserHistory(prevWord, secondWord, maxFreq > 0); + return prevWord; } /** @@ -2526,7 +2528,7 @@ public final class LatinIME extends InputMethodService implements KeyboardAction final CharSequence[] items = new CharSequence[] { // TODO: Should use new string "Select active input modes". getString(R.string.language_selection_title), - getString(R.string.english_ime_settings), + getString(Utils.getAcitivityTitleResId(this, SettingsActivity.class)), }; final DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() { @Override diff --git a/java/src/com/android/inputmethod/latin/RichInputConnection.java b/java/src/com/android/inputmethod/latin/RichInputConnection.java index 8a7ade49e..16744d1f0 100644 --- a/java/src/com/android/inputmethod/latin/RichInputConnection.java +++ b/java/src/com/android/inputmethod/latin/RichInputConnection.java @@ -60,11 +60,11 @@ public final class RichInputConnection { * This contains the committed text immediately preceding the cursor and the composing * text if any. It is refreshed when the cursor moves by calling upon the TextView. */ - private StringBuilder mCommittedTextBeforeComposingText = new StringBuilder(); + private final StringBuilder mCommittedTextBeforeComposingText = new StringBuilder(); /** * This contains the currently composing text, as LatinIME thinks the TextView is seeing it. */ - private StringBuilder mComposingText = new StringBuilder(); + private final StringBuilder mComposingText = new StringBuilder(); // A hint on how many characters to cache from the TextView. A good value of this is given by // how many characters we need to be able to almost always find the caps mode. private static final int DEFAULT_TEXT_CACHE_SIZE = 100; @@ -334,13 +334,15 @@ public final class RichInputConnection { mCurrentCursorPosition = end; final CharSequence textBeforeCursor = getTextBeforeCursor(DEFAULT_TEXT_CACHE_SIZE + (end - start), 0); - final int indexOfStartOfComposingText = - Math.max(textBeforeCursor.length() - (end - start), 0); - mComposingText.append(textBeforeCursor.subSequence(indexOfStartOfComposingText, - textBeforeCursor.length())); mCommittedTextBeforeComposingText.setLength(0); - mCommittedTextBeforeComposingText.append( - textBeforeCursor.subSequence(0, indexOfStartOfComposingText)); + if (!TextUtils.isEmpty(textBeforeCursor)) { + final int indexOfStartOfComposingText = + Math.max(textBeforeCursor.length() - (end - start), 0); + mComposingText.append(textBeforeCursor.subSequence(indexOfStartOfComposingText, + textBeforeCursor.length())); + mCommittedTextBeforeComposingText.append( + textBeforeCursor.subSequence(0, indexOfStartOfComposingText)); + } if (null != mIC) { mIC.setComposingRegion(start, end); } @@ -502,16 +504,6 @@ public final class RichInputConnection { return (r == null) ? null : r.mWord; } - private int getCursorPosition() { - mIC = mParent.getCurrentInputConnection(); - if (null == mIC) return INVALID_CURSOR_POSITION; - final ExtractedText extracted = mIC.getExtractedText(new ExtractedTextRequest(), 0); - if (extracted == null) { - return INVALID_CURSOR_POSITION; - } - return extracted.startOffset + extracted.selectionStart; - } - /** * Returns the text surrounding the cursor. * diff --git a/java/src/com/android/inputmethod/latin/SettingsFragment.java b/java/src/com/android/inputmethod/latin/SettingsFragment.java index 4fdd83911..928141c32 100644 --- a/java/src/com/android/inputmethod/latin/SettingsFragment.java +++ b/java/src/com/android/inputmethod/latin/SettingsFragment.java @@ -69,6 +69,11 @@ public final class SettingsFragment extends InputMethodSettingsFragment setInputMethodSettingsCategoryTitle(R.string.language_selection_title); setSubtypeEnablerTitle(R.string.select_language); addPreferencesFromResource(R.xml.prefs); + final PreferenceScreen preferenceScreen = getPreferenceScreen(); + if (preferenceScreen != null) { + preferenceScreen.setTitle( + Utils.getAcitivityTitleResId(getActivity(), SettingsActivity.class)); + } final Resources res = getResources(); final Context context = getActivity(); diff --git a/java/src/com/android/inputmethod/latin/StringUtils.java b/java/src/com/android/inputmethod/latin/StringUtils.java index 90c3fcdd2..59ad28fc9 100644 --- a/java/src/com/android/inputmethod/latin/StringUtils.java +++ b/java/src/com/android/inputmethod/latin/StringUtils.java @@ -22,6 +22,10 @@ import java.util.ArrayList; import java.util.Locale; public final class StringUtils { + public static final int CAPITALIZE_NONE = 0; // No caps, or mixed case + public static final int CAPITALIZE_FIRST = 1; // First only + public static final int CAPITALIZE_ALL = 2; // All caps + private StringUtils() { // This utility class is not publicly instantiable. } @@ -111,11 +115,12 @@ public final class StringUtils { // - This does not work for Greek, because it returns upper case instead of title case. // - It does not work for Serbian, because it fails to account for the "lj" character, // which should be "Lj" in title case and "LJ" in upper case. - // - It does not work for Dutch, because it fails to account for the "ij" digraph, which - // are two different characters but both should be capitalized as "IJ" as if they were - // a single letter. - // - It also does not work with unicode surrogate code points. - return s.toUpperCase(locale).charAt(0) + s.substring(1); + // - It does not work for Dutch, because it fails to account for the "ij" digraph when it's + // written as two separate code points. They are two different characters but both should + // be capitalized as "IJ" as if they were a single letter in most words (not all). If the + // unicode char for the ligature is used however, it works. + final int cutoff = s.offsetByCodePoints(0, 1); + return s.substring(0, cutoff).toUpperCase(locale) + s.substring(cutoff).toLowerCase(locale); } private static final int[] EMPTY_CODEPOINTS = {}; @@ -171,4 +176,41 @@ public final class StringUtils { } return list.toArray(new String[list.size()]); } + + // This method assumes the text is not null. For the empty string, it returns CAPITALIZE_NONE. + public static int getCapitalizationType(final String text) { + // If the first char is not uppercase, then the word is either all lower case or + // camel case, and in either case we return CAPITALIZE_NONE. + final int len = text.length(); + int index = 0; + for (; index < len; index = text.offsetByCodePoints(index, 1)) { + if (Character.isLetter(text.codePointAt(index))) { + break; + } + } + if (index == len) return CAPITALIZE_NONE; + if (!Character.isUpperCase(text.codePointAt(index))) { + return CAPITALIZE_NONE; + } + int capsCount = 1; + int letterCount = 1; + for (index = text.offsetByCodePoints(index, 1); index < len; + index = text.offsetByCodePoints(index, 1)) { + if (1 != capsCount && letterCount != capsCount) break; + final int codePoint = text.codePointAt(index); + if (Character.isUpperCase(codePoint)) { + ++capsCount; + ++letterCount; + } else if (Character.isLetter(codePoint)) { + // We need to discount non-letters since they may not be upper-case, but may + // still be part of a word (e.g. single quote or dash, as in "IT'S" or "FULL-TIME") + ++letterCount; + } + } + // We know the first char is upper case. So we want to test if either every letter other + // than the first is lower case, or if they are all upper case. If the string is exactly + // one char long, then we will arrive here with letterCount 1, and this is correct, too. + if (1 == capsCount) return CAPITALIZE_FIRST; + return (letterCount == capsCount ? CAPITALIZE_ALL : CAPITALIZE_NONE); + } } diff --git a/java/src/com/android/inputmethod/latin/SubtypeLocale.java b/java/src/com/android/inputmethod/latin/SubtypeLocale.java index 9cbfe6698..5e28cc2d0 100644 --- a/java/src/com/android/inputmethod/latin/SubtypeLocale.java +++ b/java/src/com/android/inputmethod/latin/SubtypeLocale.java @@ -114,7 +114,7 @@ public final class SubtypeLocale { final String[] keyboardLayoutSetMap = res.getStringArray( R.array.locale_and_extra_value_to_keyboard_layout_set_map); - for (int i = 0; i < keyboardLayoutSetMap.length; i += 2) { + for (int i = 0; i + 1 < keyboardLayoutSetMap.length; i += 2) { final String key = keyboardLayoutSetMap[i]; final String keyboardLayoutSet = keyboardLayoutSetMap[i + 1]; sLocaleAndExtraValueToKeyboardLayoutSetMap.put(key, keyboardLayoutSet); diff --git a/java/src/com/android/inputmethod/latin/UserHistoryDictIOUtils.java b/java/src/com/android/inputmethod/latin/UserHistoryDictIOUtils.java index 62f2a9750..10931555e 100644 --- a/java/src/com/android/inputmethod/latin/UserHistoryDictIOUtils.java +++ b/java/src/com/android/inputmethod/latin/UserHistoryDictIOUtils.java @@ -207,7 +207,12 @@ public final class UserHistoryDictIOUtils { final ArrayList<PendingAttribute> attrList = bigrams.get(entry.getKey()); if (attrList != null) { for (final PendingAttribute attr : attrList) { - to.setBigram(word1, unigrams.get(attr.mAddress), + final String word2 = unigrams.get(attr.mAddress); + if (word1 == null || word2 == null) { + Log.e(TAG, "Invalid bigram pair detected: " + word1 + ", " + word2); + continue; + } + to.setBigram(word1, word2, BinaryDictInputOutput.reconstructBigramFrequency(unigramFrequency, attr.mFrequency)); } diff --git a/java/src/com/android/inputmethod/latin/Utils.java b/java/src/com/android/inputmethod/latin/Utils.java index 7a604dc6a..aff5d17d7 100644 --- a/java/src/com/android/inputmethod/latin/Utils.java +++ b/java/src/com/android/inputmethod/latin/Utils.java @@ -16,8 +16,13 @@ package com.android.inputmethod.latin; +import android.app.Activity; +import android.content.ComponentName; +import android.content.Context; import android.content.Intent; +import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; import android.inputmethodservice.InputMethodService; import android.net.Uri; import android.os.AsyncTask; @@ -45,6 +50,8 @@ import java.util.Date; import java.util.Locale; public final class Utils { + private static final String TAG = Utils.class.getSimpleName(); + private Utils() { // This utility class is not publicly instantiable. } @@ -453,4 +460,17 @@ public final class Utils { if (TextUtils.isEmpty(info)) return null; return info; } + + public static int getAcitivityTitleResId(Context context, Class<? extends Activity> cls) { + final ComponentName cn = new ComponentName(context, cls); + try { + final ActivityInfo ai = context.getPackageManager().getActivityInfo(cn, 0); + if (ai != null) { + return ai.labelRes; + } + } catch (NameNotFoundException e) { + Log.e(TAG, "Failed to get settings activity title res id.", e); + } + return 0; + } } diff --git a/java/src/com/android/inputmethod/latin/XmlParseUtils.java b/java/src/com/android/inputmethod/latin/XmlParseUtils.java index f01d4c5e6..48e5ed30a 100644 --- a/java/src/com/android/inputmethod/latin/XmlParseUtils.java +++ b/java/src/com/android/inputmethod/latin/XmlParseUtils.java @@ -30,50 +30,53 @@ public final class XmlParseUtils { @SuppressWarnings("serial") public static class ParseException extends XmlPullParserException { - public ParseException(String msg, XmlPullParser parser) { + public ParseException(final String msg, final XmlPullParser parser) { super(msg + " at " + parser.getPositionDescription()); } } @SuppressWarnings("serial") public static final class IllegalStartTag extends ParseException { - public IllegalStartTag(XmlPullParser parser, String parent) { - super("Illegal start tag " + parser.getName() + " in " + parent, parser); + public IllegalStartTag(final XmlPullParser parser, final String tag, final String parent) { + super("Illegal start tag " + tag + " in " + parent, parser); } } @SuppressWarnings("serial") public static final class IllegalEndTag extends ParseException { - public IllegalEndTag(XmlPullParser parser, String parent) { - super("Illegal end tag " + parser.getName() + " in " + parent, parser); + public IllegalEndTag(final XmlPullParser parser, final String tag, final String parent) { + super("Illegal end tag " + tag + " in " + parent, parser); } } @SuppressWarnings("serial") public static final class IllegalAttribute extends ParseException { - public IllegalAttribute(XmlPullParser parser, String attribute) { - super("Tag " + parser.getName() + " has illegal attribute " + attribute, parser); + public IllegalAttribute(final XmlPullParser parser, final String tag, + final String attribute) { + super("Tag " + tag + " has illegal attribute " + attribute, parser); } } @SuppressWarnings("serial") public static final class NonEmptyTag extends ParseException{ - public NonEmptyTag(String tag, XmlPullParser parser) { + public NonEmptyTag(final XmlPullParser parser, final String tag) { super(tag + " must be empty tag", parser); } } - public static void checkEndTag(String tag, XmlPullParser parser) + public static void checkEndTag(final String tag, final XmlPullParser parser) throws XmlPullParserException, IOException { if (parser.next() == XmlPullParser.END_TAG && tag.equals(parser.getName())) return; - throw new NonEmptyTag(tag, parser); + throw new NonEmptyTag(parser, tag); } - public static void checkAttributeExists(TypedArray attr, int attrId, String attrName, - String tag, XmlPullParser parser) throws XmlPullParserException { - if (attr.hasValue(attrId)) + public static void checkAttributeExists(final TypedArray attr, final int attrId, + final String attrName, final String tag, final XmlPullParser parser) + throws XmlPullParserException { + if (attr.hasValue(attrId)) { return; + } throw new ParseException( "No " + attrName + " attribute found in <" + tag + "/>", parser); } diff --git a/java/src/com/android/inputmethod/latin/define/ProductionFlag.java b/java/src/com/android/inputmethod/latin/define/ProductionFlag.java index 699e47b6a..dc937fb25 100644 --- a/java/src/com/android/inputmethod/latin/define/ProductionFlag.java +++ b/java/src/com/android/inputmethod/latin/define/ProductionFlag.java @@ -28,5 +28,5 @@ public final class ProductionFlag { // USES_DEVELOPMENT_ONLY_DIAGNOSTICS must be false for any production build. public static final boolean USES_DEVELOPMENT_ONLY_DIAGNOSTICS_DEBUG = false; - public static final boolean IS_HARDWARE_KEYBOARD_SUPPORTED = true; + public static final boolean IS_HARDWARE_KEYBOARD_SUPPORTED = false; } diff --git a/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java b/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java index 5c805598a..e7c7e2b8a 100644 --- a/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java +++ b/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java @@ -620,34 +620,34 @@ public final class FusionDictionary implements Iterable<Word> { * Helper method to find a word in a given branch. */ @SuppressWarnings("unused") - public static CharGroup findWordInTree(Node node, final String s) { + public static CharGroup findWordInTree(Node node, final String string) { int index = 0; final StringBuilder checker = DBG ? new StringBuilder() : null; + final int[] codePoints = getCodePoints(string); CharGroup currentGroup; - final int codePointCountInS = s.codePointCount(0, s.length()); do { - int indexOfGroup = findIndexOfChar(node, s.codePointAt(index)); + int indexOfGroup = findIndexOfChar(node, codePoints[index]); if (CHARACTER_NOT_FOUND == indexOfGroup) return null; currentGroup = node.mData.get(indexOfGroup); - if (s.length() - index < currentGroup.mChars.length) return null; + if (codePoints.length - index < currentGroup.mChars.length) return null; int newIndex = index; - while (newIndex < s.length() && newIndex - index < currentGroup.mChars.length) { - if (currentGroup.mChars[newIndex - index] != s.codePointAt(newIndex)) return null; + while (newIndex < codePoints.length && newIndex - index < currentGroup.mChars.length) { + if (currentGroup.mChars[newIndex - index] != codePoints[newIndex]) return null; newIndex++; } index = newIndex; if (DBG) checker.append(new String(currentGroup.mChars, 0, currentGroup.mChars.length)); - if (index < codePointCountInS) { + if (index < codePoints.length) { node = currentGroup.mChildren; } - } while (null != node && index < codePointCountInS); + } while (null != node && index < codePoints.length); - if (index < codePointCountInS) return null; + if (index < codePoints.length) return null; if (!currentGroup.isTerminal()) return null; - if (DBG && !s.equals(checker.toString())) return null; + if (DBG && !codePoints.equals(checker.toString())) return null; return currentGroup; } @@ -847,12 +847,12 @@ public final class FusionDictionary implements Iterable<Word> { @Override public Word next() { Position currentPos = mPositions.getLast(); - mCurrentString.setLength(mCurrentString.length() - currentPos.length); + mCurrentString.setLength(currentPos.length); do { if (currentPos.pos.hasNext()) { final CharGroup currentGroup = currentPos.pos.next(); - currentPos.length = currentGroup.mChars.length; + currentPos.length = mCurrentString.length(); for (int i : currentGroup.mChars) mCurrentString.append(Character.toChars(i)); if (null != currentGroup.mChildren) { @@ -866,7 +866,7 @@ public final class FusionDictionary implements Iterable<Word> { } else { mPositions.removeLast(); currentPos = mPositions.getLast(); - mCurrentString.setLength(mCurrentString.length() - mPositions.getLast().length); + mCurrentString.setLength(mPositions.getLast().length); } } while (true); } diff --git a/java/src/com/android/inputmethod/latin/setup/SetupActivity.java b/java/src/com/android/inputmethod/latin/setup/SetupActivity.java index 7f66c6d3e..15d0bac37 100644 --- a/java/src/com/android/inputmethod/latin/setup/SetupActivity.java +++ b/java/src/com/android/inputmethod/latin/setup/SetupActivity.java @@ -112,12 +112,13 @@ public final class SetupActivity extends Activity { // 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); - titleView.setText(getString(R.string.setup_title, getString(R.string.english_ime_name))); + 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), - R.string.setup_step1_title, R.string.setup_step1_instruction, + 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 @@ -129,7 +130,7 @@ public final class SetupActivity extends Activity { mSetupSteps.addStep(STEP_1, step1); final SetupStep step2 = new SetupStep(findViewById(R.id.setup_step2), - R.string.setup_step2_title, R.string.setup_step2_instruction, + appName, R.string.setup_step2_title, R.string.setup_step2_instruction, 0 /* actionIcon */, R.string.select_input_method); step2.setAction(new Runnable() { @Override @@ -142,7 +143,7 @@ public final class SetupActivity extends Activity { mSetupSteps.addStep(STEP_2, step2); final SetupStep step3 = new SetupStep(findViewById(R.id.setup_step3), - R.string.setup_step3_title, 0 /* instruction */, + appName, R.string.setup_step3_title, 0 /* instruction */, R.drawable.sym_keyboard_language_switch, R.string.setup_step3_instruction); step3.setAction(new Runnable() { @Override @@ -290,11 +291,11 @@ public final class SetupActivity extends Activity { private final TextView mActionLabel; private Runnable mAction; - public SetupStep(final View rootView, final int title, final int instruction, - final int actionIcon, final int actionLabel) { + 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(R.string.english_ime_name); + final String applicationName = res.getString(appName); final TextView titleView = (TextView)rootView.findViewById(R.id.setup_step_title); titleView.setText(res.getString(title, applicationName)); diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java index 97e280d79..fbed139f3 100644 --- a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java +++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java @@ -58,10 +58,6 @@ public final class AndroidSpellCheckerService extends SpellCheckerService public static final String PREF_USE_CONTACTS_KEY = "pref_spellcheck_use_contacts"; - public static final int CAPITALIZE_NONE = 0; // No caps, or mixed case - public static final int CAPITALIZE_FIRST = 1; // First only - public static final int CAPITALIZE_ALL = 2; // All caps - private final static String[] EMPTY_STRING_ARRAY = new String[0]; private Map<String, DictionaryPool> mDictionaryPools = CollectionUtils.newSynchronizedTreeMap(); private Map<String, UserBinaryDictionary> mUserDictionaries = @@ -325,13 +321,13 @@ public final class AndroidSpellCheckerService extends SpellCheckerService } Collections.reverse(mSuggestions); StringUtils.removeDupes(mSuggestions); - if (CAPITALIZE_ALL == capitalizeType) { + if (StringUtils.CAPITALIZE_ALL == capitalizeType) { for (int i = 0; i < mSuggestions.size(); ++i) { // get(i) returns a CharSequence which is actually a String so .toString() // should return the same object. mSuggestions.set(i, mSuggestions.get(i).toString().toUpperCase(locale)); } - } else if (CAPITALIZE_FIRST == capitalizeType) { + } else if (StringUtils.CAPITALIZE_FIRST == capitalizeType) { for (int i = 0; i < mSuggestions.size(); ++i) { // Likewise mSuggestions.set(i, StringUtils.toTitleCase( @@ -434,31 +430,4 @@ public final class AndroidSpellCheckerService extends SpellCheckerService } return new DictAndProximity(dictionaryCollection, proximityInfo); } - - // This method assumes the text is not empty or null. - public static int getCapitalizationType(String text) { - // If the first char is not uppercase, then the word is either all lower case, - // and in either case we return CAPITALIZE_NONE. - if (!Character.isUpperCase(text.codePointAt(0))) return CAPITALIZE_NONE; - final int len = text.length(); - int capsCount = 1; - int letterCount = 1; - for (int i = 1; i < len; i = text.offsetByCodePoints(i, 1)) { - if (1 != capsCount && letterCount != capsCount) break; - final int codePoint = text.codePointAt(i); - if (Character.isUpperCase(codePoint)) { - ++capsCount; - ++letterCount; - } else if (Character.isLetter(codePoint)) { - // We need to discount non-letters since they may not be upper-case, but may - // still be part of a word (e.g. single quote or dash, as in "IT'S" or "FULL-TIME") - ++letterCount; - } - } - // We know the first char is upper case. So we want to test if either every letter other - // than the first is lower case, or if they are all upper case. If the string is exactly - // one char long, then we will arrive here with letterCount 1, and this is correct, too. - if (1 == capsCount) return CAPITALIZE_FIRST; - return (letterCount == capsCount ? CAPITALIZE_ALL : CAPITALIZE_NONE); - } } diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java index 4f86a3175..b15063235 100644 --- a/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java +++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java @@ -150,7 +150,7 @@ public abstract class AndroidWordLevelSpellCheckerSession extends Session { // Greek letters are either in the 370~3FF range (Greek & Coptic), or in the // 1F00~1FFF range (Greek extended). Our dictionary contains both sort of characters. // Our dictionary also contains a few words with 0xF2; it would be best to check - // if that's correct, but a Google search does return results for these words so + // if that's correct, but a web search does return results for these words so // they are probably okay. return (codePoint >= 0x370 && codePoint <= 0x3FF) || (codePoint >= 0x1F00 && codePoint <= 0x1FFF) @@ -214,14 +214,14 @@ public abstract class AndroidWordLevelSpellCheckerSession extends Session { // If the word is in there as is, then it's in the dictionary. If not, we'll test lower // case versions, but only if the word is not already all-lower case or mixed case. if (dict.isValidWord(text)) return true; - if (AndroidSpellCheckerService.CAPITALIZE_NONE == capitalizeType) return false; + if (StringUtils.CAPITALIZE_NONE == capitalizeType) return false; // If we come here, we have a capitalized word (either First- or All-). // Downcase the word and look it up again. If the word is only capitalized, we // tested all possibilities, so if it's still negative we can return false. final String lowerCaseText = text.toLowerCase(mLocale); if (dict.isValidWord(lowerCaseText)) return true; - if (AndroidSpellCheckerService.CAPITALIZE_FIRST == capitalizeType) return false; + if (StringUtils.CAPITALIZE_FIRST == capitalizeType) return false; // If the lower case version is not in the dictionary, it's still possible // that we have an all-caps version of a word that needs to be capitalized @@ -296,7 +296,7 @@ public abstract class AndroidWordLevelSpellCheckerSession extends Session { } } - final int capitalizeType = AndroidSpellCheckerService.getCapitalizationType(text); + final int capitalizeType = StringUtils.getCapitalizationType(text); boolean isInDict = true; DictAndProximity dictInfo = null; try { diff --git a/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerSettingsFragment.java b/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerSettingsFragment.java index 9606b0352..5ce9d8e47 100644 --- a/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerSettingsFragment.java +++ b/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerSettingsFragment.java @@ -18,8 +18,10 @@ package com.android.inputmethod.latin.spellcheck; import android.os.Bundle; import android.preference.PreferenceFragment; +import android.preference.PreferenceScreen; import com.android.inputmethod.latin.R; +import com.android.inputmethod.latin.Utils; /** * Preference screen. @@ -35,5 +37,10 @@ public final class SpellCheckerSettingsFragment extends PreferenceFragment { public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); addPreferencesFromResource(R.xml.spell_checker_settings); + final PreferenceScreen preferenceScreen = getPreferenceScreen(); + if (preferenceScreen != null) { + preferenceScreen.setTitle(Utils.getAcitivityTitleResId( + getActivity(), SpellCheckerSettingsActivity.class)); + } } } diff --git a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java index 8c3d3b08c..eeaf828a7 100644 --- a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java +++ b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java @@ -62,6 +62,7 @@ import com.android.inputmethod.latin.LatinImeLogger; import com.android.inputmethod.latin.R; import com.android.inputmethod.latin.ResourceUtils; import com.android.inputmethod.latin.SuggestedWords; +import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; import com.android.inputmethod.latin.Utils; import com.android.inputmethod.latin.define.ProductionFlag; import com.android.inputmethod.research.ResearchLogger; @@ -72,7 +73,7 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick OnLongClickListener { public interface Listener { public void addWordToUserDictionary(String word); - public void pickSuggestionManually(int index, String word); + public void pickSuggestionManually(int index, SuggestedWordInfo word); } // The maximum number of suggestions available. See {@link Suggest#mPrefMaxSuggestions}. @@ -656,8 +657,8 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick @Override public boolean onCustomRequest(final int requestCode) { final int index = requestCode; - final String word = mSuggestedWords.getWord(index); - mListener.pickSuggestionManually(index, word); + final SuggestedWordInfo wordInfo = mSuggestedWords.getInfo(index); + mListener.pickSuggestionManually(index, wordInfo); dismissMoreSuggestions(); return true; } @@ -807,8 +808,8 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick if (index >= mSuggestedWords.size()) return; - final String word = mSuggestedWords.getWord(index); - mListener.pickSuggestionManually(index, word); + final SuggestedWordInfo wordInfo = mSuggestedWords.getInfo(index); + mListener.pickSuggestionManually(index, wordInfo); } @Override diff --git a/java/src/com/android/inputmethod/research/BootBroadcastReceiver.java b/java/src/com/android/inputmethod/research/BootBroadcastReceiver.java index c5f095919..4f86526a7 100644 --- a/java/src/com/android/inputmethod/research/BootBroadcastReceiver.java +++ b/java/src/com/android/inputmethod/research/BootBroadcastReceiver.java @@ -25,9 +25,10 @@ import android.content.Intent; */ public final class BootBroadcastReceiver extends BroadcastReceiver { @Override - public void onReceive(Context context, Intent intent) { + public void onReceive(final Context context, final Intent intent) { if (intent.getAction().equals(Intent.ACTION_BOOT_COMPLETED)) { - ResearchLogger.scheduleUploadingService(context); + UploaderService.cancelAndRescheduleUploadingService(context, + true /* needsRescheduling */); } } } diff --git a/java/src/com/android/inputmethod/research/ResearchLogger.java b/java/src/com/android/inputmethod/research/ResearchLogger.java index a38a226f0..e0bd37c1e 100644 --- a/java/src/com/android/inputmethod/research/ResearchLogger.java +++ b/java/src/com/android/inputmethod/research/ResearchLogger.java @@ -20,16 +20,13 @@ import static com.android.inputmethod.latin.Constants.Subtype.ExtraValue.KEYBOAR import android.accounts.Account; import android.accounts.AccountManager; -import android.app.AlarmManager; import android.app.AlertDialog; import android.app.Dialog; -import android.app.PendingIntent; import android.content.Context; import android.content.DialogInterface; import android.content.DialogInterface.OnCancelListener; import android.content.Intent; import android.content.SharedPreferences; -import android.content.SharedPreferences.Editor; import android.content.pm.PackageInfo; import android.content.pm.PackageManager.NameNotFoundException; import android.content.res.Resources; @@ -74,22 +71,16 @@ import com.android.inputmethod.latin.SuggestedWords; import com.android.inputmethod.latin.define.ProductionFlag; import com.android.inputmethod.research.MotionEventReader.ReplayData; -import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; -import java.io.InputStreamReader; import java.nio.MappedByteBuffer; import java.nio.channels.FileChannel; import java.nio.charset.Charset; -import java.text.SimpleDateFormat; import java.util.ArrayList; -import java.util.Date; import java.util.List; -import java.util.Locale; import java.util.Random; -import java.util.UUID; /** * Logs the use of the LatinIME keyboard. @@ -254,7 +245,8 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang mUploadNowIntent = new Intent(mLatinIME, UploaderService.class); mUploadNowIntent.putExtra(UploaderService.EXTRA_UPLOAD_UNCONDITIONALLY, true); if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - scheduleUploadingService(mLatinIME); + UploaderService.cancelAndRescheduleUploadingService(mLatinIME, + true /* needsRescheduling */); } mReplayer.setKeyboardSwitcher(keyboardSwitcher); } @@ -268,25 +260,6 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang ResearchSettings.writeResearchLastDirCleanupTime(mPrefs, now); } - /** - * Arrange for the UploaderService to be run on a regular basis. - * - * Any existing scheduled invocation of UploaderService is removed and rescheduled. This may - * cause problems if this method is called often and frequent updates are required, but since - * the user will likely be sleeping at some point, if the interval is less that the expected - * sleep duration and this method is not called during that time, the service should be invoked - * at some point. - */ - public static void scheduleUploadingService(Context context) { - final Intent intent = new Intent(context, UploaderService.class); - final PendingIntent pendingIntent = PendingIntent.getService(context, 0, intent, 0); - final AlarmManager manager = - (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); - manager.cancel(pendingIntent); - manager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP, - UploaderService.RUN_INTERVAL, UploaderService.RUN_INTERVAL, pendingIntent); - } - public void mainKeyboardView_onAttachedToWindow(final MainKeyboardView mainKeyboardView) { mMainKeyboardView = mainKeyboardView; maybeShowSplashScreen(); @@ -790,8 +763,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang } private boolean isAllowedToLog() { - return !mIsPasswordView && !mIsLoggingSuspended && sIsLogging && !mInFeedbackDialog - && !isReplaying(); + return !mIsPasswordView && !mIsLoggingSuspended && sIsLogging && !mInFeedbackDialog; } public void requestIndicatorRedraw() { @@ -1122,10 +1094,6 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang } } - public void latinIME_onFinishInputViewInternal() { - stop(); - } - /** * Log a change in preferences. * @@ -1208,16 +1176,22 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang } /** - * Log a call to LatinIME.onWindowHidden(). + * The IME is finishing; it is either being destroyed, or is about to be hidden. * * UserAction: The user has performed an action that has caused the IME to be closed. They may * have focused on something other than a text field, or explicitly closed it. */ - private static final LogStatement LOGSTATEMENT_LATINIME_ONWINDOWHIDDEN = - new LogStatement("LatinIMEOnWindowHidden", false, false, "isTextTruncated", "text"); - public static void latinIME_onWindowHidden(final int savedSelectionStart, - final int savedSelectionEnd, final InputConnection ic) { - if (ic != null) { + private static final LogStatement LOGSTATEMENT_LATINIME_ONFINISHINPUTVIEWINTERNAL = + new LogStatement("LatinIMEOnFinishInputViewInternal", false, false, "isTextTruncated", + "text"); + public static void latinIME_onFinishInputViewInternal(final boolean finishingInput, + final int savedSelectionStart, final int savedSelectionEnd, final InputConnection ic) { + // The finishingInput flag is set in InputMethodService. It is true if called from + // doFinishInput(), which can be called as part of doStartInput(). This can happen at times + // when the IME is not closing, such as when powering up. The finishinInput flag is false + // if called from finishViews(), which is called from hideWindow() and onDestroy(). These + // are the situations in which we want to finish up the researchLog. + if (ic != null && !finishingInput) { final boolean isTextTruncated; final String text; if (LOG_FULL_TEXTVIEW_CONTENTS) { @@ -1261,8 +1235,8 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang // Assume that OUTPUT_ENTIRE_BUFFER is only true when we don't care about privacy (e.g. // during a live user test), so the normal isPotentiallyPrivate and // isPotentiallyRevealing flags do not apply - researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_ONWINDOWHIDDEN, isTextTruncated, - text); + researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_ONFINISHINPUTVIEWINTERNAL, + isTextTruncated, text); researchLogger.commitCurrentLogUnit(); getInstance().stop(); } @@ -1634,8 +1608,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang final String scrubbedAutoCorrection = scrubDigitsFromString(autoCorrection); final ResearchLogger researchLogger = getInstance(); researchLogger.mCurrentLogUnit.initializeSuggestions(suggestedWords); - researchLogger.commitCurrentLogUnitAsWord(scrubbedAutoCorrection, Long.MAX_VALUE, - isBatchMode); + researchLogger.onWordFinished(scrubbedAutoCorrection, isBatchMode); // Add the autocorrection logStatement at the end of the logUnit for the committed word. // We have to do this after calling commitCurrentLogUnitAsWord, because it may split the diff --git a/java/src/com/android/inputmethod/research/UploaderService.java b/java/src/com/android/inputmethod/research/UploaderService.java index 6a9f5c1f4..6a9717b7c 100644 --- a/java/src/com/android/inputmethod/research/UploaderService.java +++ b/java/src/com/android/inputmethod/research/UploaderService.java @@ -18,6 +18,8 @@ package com.android.inputmethod.research; import android.app.AlarmManager; import android.app.IntentService; +import android.app.PendingIntent; +import android.content.Context; import android.content.Intent; import android.os.Bundle; @@ -43,11 +45,17 @@ public final class UploaderService extends IntentService { @Override protected void onHandleIntent(final Intent intent) { + // We may reach this point either because the alarm fired, or because the system explicitly + // requested that an Upload occur. In the latter case, we want to cancel the alarm in case + // it's about to fire. + cancelAndRescheduleUploadingService(this, false /* needsRescheduling */); + final Uploader uploader = new Uploader(this); if (!uploader.isPossibleToUpload()) return; if (isUploadingUnconditionally(intent.getExtras()) || uploader.isConvenientToUpload()) { uploader.doUpload(); } + cancelAndRescheduleUploadingService(this, true /* needsRescheduling */); } private boolean isUploadingUnconditionally(final Bundle bundle) { @@ -57,4 +65,42 @@ public final class UploaderService extends IntentService { } return false; } + + /** + * Arrange for the UploaderService to be run on a regular basis. + * + * Any existing scheduled invocation of UploaderService is removed and optionally rescheduled. + * This may cause problems if this method is called so often that no scheduled invocation is + * ever run. But if the delay is short enough that it will go off when the user is sleeping, + * then there should be no starvation. + * + * @param context {@link Context} object + * @param needsRescheduling whether to schedule a future intent to be delivered to this service + */ + public static void cancelAndRescheduleUploadingService(final Context context, + final boolean needsRescheduling) { + final PendingIntent pendingIntent = getPendingIntentForService(context); + final AlarmManager alarmManager = (AlarmManager) context.getSystemService( + Context.ALARM_SERVICE); + cancelAnyScheduledServiceAlarm(alarmManager, pendingIntent); + if (needsRescheduling) { + scheduleServiceAlarm(alarmManager, 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); + } } |