diff options
Diffstat (limited to 'java/src')
33 files changed, 1103 insertions, 968 deletions
diff --git a/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java b/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java index 810bd9150..f3cfae203 100644 --- a/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java +++ b/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java @@ -16,10 +16,7 @@ package com.android.inputmethod.keyboard; -import android.animation.Animator; import android.animation.AnimatorInflater; -import android.animation.AnimatorListenerAdapter; -import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.content.Context; import android.content.SharedPreferences; @@ -35,13 +32,10 @@ import android.preference.PreferenceManager; import android.util.AttributeSet; import android.util.DisplayMetrics; import android.util.Log; -import android.util.TypedValue; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; -import android.view.animation.AccelerateInterpolator; -import android.view.animation.DecelerateInterpolator; import android.view.inputmethod.InputMethodSubtype; import android.widget.TextView; @@ -53,6 +47,7 @@ import com.android.inputmethod.keyboard.internal.DrawingPreviewPlacerView; import com.android.inputmethod.keyboard.internal.GestureFloatingTextDrawingPreview; import com.android.inputmethod.keyboard.internal.GestureTrailsDrawingPreview; import com.android.inputmethod.keyboard.internal.KeyDrawParams; +import com.android.inputmethod.keyboard.internal.KeyPreviewChoreographer; import com.android.inputmethod.keyboard.internal.KeyPreviewDrawParams; import com.android.inputmethod.keyboard.internal.NonDistinctMultitouchHelper; import com.android.inputmethod.keyboard.internal.SlidingKeyInputDrawingPreview; @@ -71,9 +66,6 @@ import com.android.inputmethod.latin.utils.UsabilityStudyLogUtils; import com.android.inputmethod.latin.utils.ViewLayoutUtils; import com.android.inputmethod.research.ResearchLogger; -import java.util.ArrayDeque; -import java.util.HashMap; -import java.util.HashSet; import java.util.WeakHashMap; /** @@ -161,25 +153,8 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack // Key preview private static final boolean FADE_OUT_KEY_TOP_LETTER_WHEN_KEY_IS_PRESSED = false; - private final int mKeyPreviewLayoutId; - private final int mKeyPreviewOffset; - private final int mKeyPreviewHeight; - // Free {@link TextView} pool that can be used for key preview. - private final ArrayDeque<TextView> mFreeKeyPreviewTextViews = CollectionUtils.newArrayDeque(); - // Map from {@link Key} to {@link TextView} that is currently being displayed as key preview. - private final HashMap<Key,TextView> mShowingKeyPreviewTextViews = CollectionUtils.newHashMap(); - private final KeyPreviewDrawParams mKeyPreviewDrawParams = new KeyPreviewDrawParams(); - private boolean mShowKeyPreviewPopup = true; - private int mKeyPreviewLingerTimeout; - private int mKeyPreviewZoomInDuration; - private int mKeyPreviewZoomOutDuration; - private static final float KEY_PREVIEW_START_ZOOM_IN_SCALE = 0.7f; - private static final float KEY_PREVIEW_END_ZOOM_IN_SCALE = 1.0f; - private static final float KEY_PREVIEW_END_ZOOM_OUT_SCALE = 0.7f; - private static final AccelerateInterpolator ACCELERATE_INTERPOLATOR = - new AccelerateInterpolator(); - private static final DecelerateInterpolator DECELERATE_INTERPOLATOR = - new DecelerateInterpolator(); + private final KeyPreviewDrawParams mKeyPreviewDrawParams; + private final KeyPreviewChoreographer mKeyPreviewChoreographer; // More keys keyboard private final Paint mBackgroundDimAlphaPaint = new Paint(); @@ -267,21 +242,9 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack final int altCodeKeyWhileTypingFadeinAnimatorResId = mainKeyboardViewAttr.getResourceId( R.styleable.MainKeyboardView_altCodeKeyWhileTypingFadeinAnimator, 0); - mKeyPreviewOffset = mainKeyboardViewAttr.getDimensionPixelOffset( - R.styleable.MainKeyboardView_keyPreviewOffset, 0); - mKeyPreviewHeight = mainKeyboardViewAttr.getDimensionPixelSize( - R.styleable.MainKeyboardView_keyPreviewHeight, 0); - mKeyPreviewLingerTimeout = mainKeyboardViewAttr.getInt( - R.styleable.MainKeyboardView_keyPreviewLingerTimeout, 0); - mKeyPreviewLayoutId = mainKeyboardViewAttr.getResourceId( - R.styleable.MainKeyboardView_keyPreviewLayout, 0); - if (mKeyPreviewLayoutId == 0) { - mShowKeyPreviewPopup = false; - } - mKeyPreviewZoomInDuration = mainKeyboardViewAttr.getInt( - R.styleable.MainKeyboardView_keyPreviewZoomInDuration, 0); - mKeyPreviewZoomOutDuration = mainKeyboardViewAttr.getInt( - R.styleable.MainKeyboardView_keyPreviewZoomOutDuration, 0); + mKeyPreviewDrawParams = new KeyPreviewDrawParams(mainKeyboardViewAttr); + mKeyPreviewChoreographer = new KeyPreviewChoreographer(mKeyPreviewDrawParams); + final int moreKeysKeyboardLayoutId = mainKeyboardViewAttr.getResourceId( R.styleable.MainKeyboardView_moreKeysKeyboardLayout, 0); mConfigShowMoreKeysKeyboardAtTouchedPoint = mainKeyboardViewAttr.getBoolean( @@ -455,8 +418,7 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack * @see #isKeyPreviewPopupEnabled() */ public void setKeyPreviewPopupEnabled(final boolean previewEnabled, final int delay) { - mShowKeyPreviewPopup = previewEnabled; - mKeyPreviewLingerTimeout = delay; + mKeyPreviewDrawParams.setPopupEnabled(previewEnabled, delay); } private void locatePreviewPlacerView() { @@ -496,62 +458,16 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack * @see #setKeyPreviewPopupEnabled(boolean, int) */ public boolean isKeyPreviewPopupEnabled() { - return mShowKeyPreviewPopup; - } - - private TextView getKeyPreviewTextView(final Key key) { - TextView previewTextView = mShowingKeyPreviewTextViews.remove(key); - if (previewTextView != null) { - return previewTextView; - } - previewTextView = mFreeKeyPreviewTextViews.poll(); - if (previewTextView != null) { - return previewTextView; - } - final Context context = getContext(); - if (mKeyPreviewLayoutId != 0) { - previewTextView = (TextView)LayoutInflater.from(context) - .inflate(mKeyPreviewLayoutId, null); - } else { - previewTextView = new TextView(context); - } - locatePreviewPlacerView(); - mDrawingPreviewPlacerView.addView( - previewTextView, ViewLayoutUtils.newLayoutParam(mDrawingPreviewPlacerView, 0, 0)); - return previewTextView; + return mKeyPreviewDrawParams.isPopupEnabled(); } // Implements {@link DrawingHandler.Callbacks} method. @Override public void dismissAllKeyPreviews() { - for (final Key key : new HashSet<Key>(mShowingKeyPreviewTextViews.keySet())) { - dismissKeyPreviewWithoutDelay(key); - } + mKeyPreviewChoreographer.dismissAllKeyPreviews(); PointerTracker.setReleasedKeyGraphicsToAllKeys(); } - // Background state set - private static final int[][][] KEY_PREVIEW_BACKGROUND_STATE_TABLE = { - { // STATE_MIDDLE - EMPTY_STATE_SET, - { R.attr.state_has_morekeys } - }, - { // STATE_LEFT - { R.attr.state_left_edge }, - { R.attr.state_left_edge, R.attr.state_has_morekeys } - }, - { // STATE_RIGHT - { R.attr.state_right_edge }, - { R.attr.state_right_edge, R.attr.state_has_morekeys } - } - }; - private static final int STATE_MIDDLE = 0; - private static final int STATE_LEFT = 1; - private static final int STATE_RIGHT = 2; - private static final int STATE_NORMAL = 0; - private static final int STATE_HAS_MOREKEYS = 1; - - // TODO: Take this method out of this class. @Override public void showKeyPreview(final Key key) { // If key is invalid or IME is already closed, we must not show key preview. @@ -563,197 +479,36 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack final KeyPreviewDrawParams previewParams = mKeyPreviewDrawParams; final Keyboard keyboard = getKeyboard(); - if (!mShowKeyPreviewPopup) { - previewParams.mPreviewVisibleOffset = -keyboard.mVerticalGap; + if (!previewParams.isPopupEnabled()) { + previewParams.setVisibleOffset(-keyboard.mVerticalGap); return; } - final TextView previewTextView = getKeyPreviewTextView(key); - final KeyDrawParams drawParams = mKeyDrawParams; - previewTextView.setTextColor(drawParams.mPreviewTextColor); - final Drawable background = previewTextView.getBackground(); - final String label = key.getPreviewLabel(); - // What we show as preview should match what we show on a key top in onDraw(). - if (label != null) { - // TODO Should take care of temporaryShiftLabel here. - previewTextView.setCompoundDrawables(null, null, null, null); - previewTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, - key.selectPreviewTextSize(drawParams)); - previewTextView.setTypeface(key.selectPreviewTypeface(drawParams)); - previewTextView.setText(label); - } else { - previewTextView.setCompoundDrawables(null, null, null, - key.getPreviewIcon(keyboard.mIconsSet)); - previewTextView.setText(null); - } - - previewTextView.measure( - ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); - final int keyDrawWidth = key.getDrawWidth(); - final int previewWidth = previewTextView.getMeasuredWidth(); - final int previewHeight = mKeyPreviewHeight; - // The width and height of visible part of the key preview background. The content marker - // of the background 9-patch have to cover the visible part of the background. - previewParams.mPreviewVisibleWidth = previewWidth - previewTextView.getPaddingLeft() - - previewTextView.getPaddingRight(); - previewParams.mPreviewVisibleHeight = previewHeight - previewTextView.getPaddingTop() - - previewTextView.getPaddingBottom(); - // The distance between the top edge of the parent key and the bottom of the visible part - // of the key preview background. - previewParams.mPreviewVisibleOffset = - mKeyPreviewOffset - previewTextView.getPaddingBottom(); + locatePreviewPlacerView(); + final TextView previewTextView = mKeyPreviewChoreographer.getKeyPreviewTextView( + key, mDrawingPreviewPlacerView); getLocationInWindow(mOriginCoords); - // The key preview is horizontally aligned with the center of the visible part of the - // parent key. If it doesn't fit in this {@link KeyboardView}, it is moved inward to fit and - // the left/right background is used if such background is specified. - final int statePosition; - int previewX = key.getDrawX() - (previewWidth - keyDrawWidth) / 2 - + CoordinateUtils.x(mOriginCoords); - if (previewX < 0) { - previewX = 0; - statePosition = STATE_LEFT; - } else if (previewX > getWidth() - previewWidth) { - previewX = getWidth() - previewWidth; - statePosition = STATE_RIGHT; - } else { - statePosition = STATE_MIDDLE; - } - // The key preview is placed vertically above the top edge of the parent key with an - // arbitrary offset. - final int previewY = key.getY() - previewHeight + mKeyPreviewOffset - + CoordinateUtils.y(mOriginCoords); - - if (background != null) { - final int hasMoreKeys = (key.getMoreKeys() != null) ? STATE_HAS_MOREKEYS : STATE_NORMAL; - background.setState(KEY_PREVIEW_BACKGROUND_STATE_TABLE[statePosition][hasMoreKeys]); - } - ViewLayoutUtils.placeViewAt( - previewTextView, previewX, previewY, previewWidth, previewHeight); - - if (!isHardwareAccelerated()) { - previewTextView.setVisibility(VISIBLE); - mShowingKeyPreviewTextViews.put(key, previewTextView); - return; - } - previewTextView.setPivotX(previewWidth / 2.0f); - previewTextView.setPivotY(previewHeight); - - final Animator zoomIn = createZoomInAniation(key, previewTextView); - final Animator zoomOut = createZoomOutAnimation(key, previewTextView); - final KeyPreviewAnimations animation = new KeyPreviewAnimations(zoomIn, zoomOut); - previewTextView.setTag(animation); - animation.startZoomIn(); - } - - // TODO: Move this internal class out to a separate external class. - private static class KeyPreviewAnimations extends AnimatorListenerAdapter { - private final Animator mZoomIn; - private final Animator mZoomOut; - - public KeyPreviewAnimations(final Animator zoomIn, final Animator zoomOut) { - mZoomIn = zoomIn; - mZoomOut = zoomOut; - } - - public void startZoomIn() { - mZoomIn.start(); - } - - public void startZoomOut() { - if (mZoomIn.isRunning()) { - mZoomIn.addListener(this); - return; - } - mZoomOut.start(); - } - - @Override - public void onAnimationEnd(final Animator animation) { - mZoomOut.start(); - } - } - - // TODO: Take this method out of this class. - private Animator createZoomInAniation(final Key key, final TextView previewTextView) { - final ObjectAnimator scaleXAnimation = ObjectAnimator.ofFloat( - previewTextView, SCALE_X, KEY_PREVIEW_START_ZOOM_IN_SCALE, - KEY_PREVIEW_END_ZOOM_IN_SCALE); - final ObjectAnimator scaleYAnimation = ObjectAnimator.ofFloat( - previewTextView, SCALE_Y, KEY_PREVIEW_START_ZOOM_IN_SCALE, - KEY_PREVIEW_END_ZOOM_IN_SCALE); - final AnimatorSet zoomInAnimation = new AnimatorSet(); - zoomInAnimation.play(scaleXAnimation).with(scaleYAnimation); - // TODO: Implement preference option to control key preview animation duration. - zoomInAnimation.setDuration(mKeyPreviewZoomInDuration); - zoomInAnimation.setInterpolator(DECELERATE_INTERPOLATOR); - zoomInAnimation.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationStart(final Animator animation) { - previewTextView.setVisibility(VISIBLE); - mShowingKeyPreviewTextViews.put(key, previewTextView); - } - }); - return zoomInAnimation; - } - - // TODO: Take this method out of this class. - private Animator createZoomOutAnimation(final Key key, final TextView previewTextView) { - final ObjectAnimator scaleXAnimation = ObjectAnimator.ofFloat( - previewTextView, SCALE_X, KEY_PREVIEW_END_ZOOM_OUT_SCALE); - final ObjectAnimator scaleYAnimation = ObjectAnimator.ofFloat( - previewTextView, SCALE_Y, KEY_PREVIEW_END_ZOOM_OUT_SCALE); - final AnimatorSet zoomOutAnimation = new AnimatorSet(); - zoomOutAnimation.play(scaleXAnimation).with(scaleYAnimation); - // TODO: Implement preference option to control key preview animation duration. - zoomOutAnimation.setDuration(mKeyPreviewZoomOutDuration); - zoomOutAnimation.setInterpolator(ACCELERATE_INTERPOLATOR); - zoomOutAnimation.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(final Animator animation) { - dismissKeyPreviewWithoutDelay(key); - } - }); - return zoomOutAnimation; + mKeyPreviewChoreographer.placeKeyPreview(key, previewTextView, keyboard.mIconsSet, + mKeyDrawParams, getWidth(), mOriginCoords); + mKeyPreviewChoreographer.showKeyPreview(key, previewTextView, isHardwareAccelerated()); } // Implements {@link TimerHandler.Callbacks} method. - // TODO: Take this method out of this class. @Override public void dismissKeyPreviewWithoutDelay(final Key key) { - if (key == null) { - return; - } - final TextView previewTextView = mShowingKeyPreviewTextViews.remove(key); - if (previewTextView != null) { - final Object tag = previewTextView.getTag(); - if (tag instanceof Animator) { - ((Animator)tag).cancel(); - } - previewTextView.setTag(null); - previewTextView.setVisibility(INVISIBLE); - mFreeKeyPreviewTextViews.add(previewTextView); - } + mKeyPreviewChoreographer.dismissKeyPreview(key, false /* withAnimation */); // To redraw key top letter. invalidateKey(key); } - // TODO: Take this method out of this class. @Override public void dismissKeyPreview(final Key key) { - final TextView previewTextView = mShowingKeyPreviewTextViews.get(key); - if (previewTextView == null) { - return; - } if (!isHardwareAccelerated()) { // TODO: Implement preference option to control key preview method and duration. - mDrawingHandler.dismissKeyPreview(mKeyPreviewLingerTimeout, key); + mDrawingHandler.dismissKeyPreview(mKeyPreviewDrawParams.getLingerTimeout(), key); return; } - final Object tag = previewTextView.getTag(); - if (tag instanceof KeyPreviewAnimations) { - final KeyPreviewAnimations animation = (KeyPreviewAnimations)tag; - animation.startZoomOut(); - } + mKeyPreviewChoreographer.dismissKeyPreview(key, true /* withAnimation */); } public void setSlidingKeyInputPreviewEnabled(final boolean enabled) { @@ -913,7 +668,7 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack // aligned with the bottom edge of the visible part of the key preview. // {@code mPreviewVisibleOffset} has been set appropriately in // {@link KeyboardView#showKeyPreview(PointerTracker)}. - final int pointY = key.getY() + mKeyPreviewDrawParams.mPreviewVisibleOffset; + final int pointY = key.getY() + mKeyPreviewDrawParams.getVisibleOffset(); moreKeysPanel.showMoreKeysPanel(this, this, pointX, pointY, mKeyboardActionListener); tracker.onShowMoreKeysPanel(moreKeysPanel); // TODO: Implement zoom in animation of more keys panel. @@ -1126,7 +881,7 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack } // Don't draw key top letter when key preview is showing. if (FADE_OUT_KEY_TOP_LETTER_WHEN_KEY_IS_PRESSED - && mShowingKeyPreviewTextViews.containsKey(key)) { + && mKeyPreviewChoreographer.isShowingKeyPreview(key)) { // TODO: Fade out animation for the key top letter, and fade in animation for the key // background color when the user presses the key. return; diff --git a/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboard.java b/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboard.java index 670524380..2bff107eb 100644 --- a/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboard.java +++ b/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboard.java @@ -285,7 +285,7 @@ public final class MoreKeysKeyboard extends Keyboard { // {@link MoreKeysKeyboardParams#setParameters(int,int,int,int,int,int,boolean,int)}. final boolean singleMoreKeyWithPreview = parentKeyboardView.isKeyPreviewPopupEnabled() && !parentKey.noKeyPreview() && moreKeys.length == 1 - && keyPreviewDrawParams.mPreviewVisibleWidth > 0; + && keyPreviewDrawParams.getVisibleWidth() > 0; if (singleMoreKeyWithPreview) { // Use pre-computed width and height if this more keys keyboard has only one key to // mitigate visual flicker between key preview and more keys keyboard. @@ -294,8 +294,8 @@ public final class MoreKeysKeyboard extends Keyboard { // left/right/top paddings. The bottom paddings of both backgrounds don't need to // be considered because the vertical positions of both backgrounds were already // adjusted with their bottom paddings deducted. - width = keyPreviewDrawParams.mPreviewVisibleWidth; - height = keyPreviewDrawParams.mPreviewVisibleHeight + mParams.mVerticalGap; + width = keyPreviewDrawParams.getVisibleWidth(); + height = keyPreviewDrawParams.getVisibleHeight() + mParams.mVerticalGap; } else { final float padding = context.getResources().getDimension( R.dimen.config_more_keys_keyboard_key_horizontal_padding) diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyPreviewChoreographer.java b/java/src/com/android/inputmethod/keyboard/internal/KeyPreviewChoreographer.java new file mode 100644 index 000000000..df869b2bc --- /dev/null +++ b/java/src/com/android/inputmethod/keyboard/internal/KeyPreviewChoreographer.java @@ -0,0 +1,284 @@ +/* + * Copyright (C) 2014 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 android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.util.TypedValue; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.AccelerateInterpolator; +import android.view.animation.DecelerateInterpolator; +import android.widget.TextView; + +import com.android.inputmethod.keyboard.Key; +import com.android.inputmethod.latin.R; +import com.android.inputmethod.latin.utils.CollectionUtils; +import com.android.inputmethod.latin.utils.CoordinateUtils; +import com.android.inputmethod.latin.utils.ViewLayoutUtils; + +import java.util.ArrayDeque; +import java.util.HashMap; +import java.util.HashSet; + +/** + * This class controls pop up key previews. This class decides: + * - what kind of key previews should be shown. + * - where key previews should be placed. + * - how key previews should be shown and dismissed. + */ +public final class KeyPreviewChoreographer { + // Free {@link TextView} pool that can be used for key preview. + private final ArrayDeque<TextView> mFreeKeyPreviewTextViews = CollectionUtils.newArrayDeque(); + // Map from {@link Key} to {@link TextView} that is currently being displayed as key preview. + private final HashMap<Key,TextView> mShowingKeyPreviewTextViews = CollectionUtils.newHashMap(); + + private final KeyPreviewDrawParams mParams; + + public KeyPreviewChoreographer(final KeyPreviewDrawParams params) { + mParams = params; + } + + public TextView getKeyPreviewTextView(final Key key, final ViewGroup placerView) { + TextView previewTextView = mShowingKeyPreviewTextViews.remove(key); + if (previewTextView != null) { + return previewTextView; + } + previewTextView = mFreeKeyPreviewTextViews.poll(); + if (previewTextView != null) { + return previewTextView; + } + final Context context = placerView.getContext(); + if (mParams.mLayoutId != 0) { + previewTextView = (TextView)LayoutInflater.from(context) + .inflate(mParams.mLayoutId, null); + } else { + previewTextView = new TextView(context); + } + placerView.addView(previewTextView, ViewLayoutUtils.newLayoutParam(placerView, 0, 0)); + return previewTextView; + } + + public boolean isShowingKeyPreview(final Key key) { + return mShowingKeyPreviewTextViews.containsKey(key); + } + + public void dismissAllKeyPreviews() { + for (final Key key : new HashSet<Key>(mShowingKeyPreviewTextViews.keySet())) { + dismissKeyPreview(key, false /* withAnimation */); + } + } + + public void dismissKeyPreview(final Key key, final boolean withAnimation) { + if (key == null) { + return; + } + final TextView previewTextView = mShowingKeyPreviewTextViews.get(key); + if (previewTextView == null) { + return; + } + final Object tag = previewTextView.getTag(); + if (withAnimation) { + if (tag instanceof KeyPreviewAnimations) { + final KeyPreviewAnimations animation = (KeyPreviewAnimations)tag; + animation.startZoomOut(); + } + return; + } + // Dismiss preview without animation. + if (tag instanceof Animator) { + ((Animator)tag).cancel(); + } + previewTextView.setTag(null); + previewTextView.setVisibility(View.INVISIBLE); + mFreeKeyPreviewTextViews.add(previewTextView); + } + + // Background state set + private static final int[][][] KEY_PREVIEW_BACKGROUND_STATE_TABLE = { + { // STATE_MIDDLE + {}, + { R.attr.state_has_morekeys } + }, + { // STATE_LEFT + { R.attr.state_left_edge }, + { R.attr.state_left_edge, R.attr.state_has_morekeys } + }, + { // STATE_RIGHT + { R.attr.state_right_edge }, + { R.attr.state_right_edge, R.attr.state_has_morekeys } + } + }; + private static final int STATE_MIDDLE = 0; + private static final int STATE_LEFT = 1; + private static final int STATE_RIGHT = 2; + private static final int STATE_NORMAL = 0; + private static final int STATE_HAS_MOREKEYS = 1; + + public void placeKeyPreview(final Key key, final TextView previewTextView, + final KeyboardIconsSet iconsSet, final KeyDrawParams drawParams, + final int keyboardViewWidth, final int[] originCoords) { + previewTextView.setTextColor(drawParams.mPreviewTextColor); + final Drawable background = previewTextView.getBackground(); + final String label = key.getPreviewLabel(); + // What we show as preview should match what we show on a key top in onDraw(). + if (label != null) { + // TODO Should take care of temporaryShiftLabel here. + previewTextView.setCompoundDrawables(null, null, null, null); + previewTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, + key.selectPreviewTextSize(drawParams)); + previewTextView.setTypeface(key.selectPreviewTypeface(drawParams)); + previewTextView.setText(label); + } else { + previewTextView.setCompoundDrawables(null, null, null, key.getPreviewIcon(iconsSet)); + previewTextView.setText(null); + } + + previewTextView.measure( + ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); + mParams.setGeometry(previewTextView); + final int previewWidth = previewTextView.getMeasuredWidth(); + final int previewHeight = mParams.mPreviewHeight; + final int keyDrawWidth = key.getDrawWidth(); + // The key preview is horizontally aligned with the center of the visible part of the + // parent key. If it doesn't fit in this {@link KeyboardView}, it is moved inward to fit and + // the left/right background is used if such background is specified. + final int statePosition; + int previewX = key.getDrawX() - (previewWidth - keyDrawWidth) / 2 + + CoordinateUtils.x(originCoords); + if (previewX < 0) { + previewX = 0; + statePosition = STATE_LEFT; + } else if (previewX > keyboardViewWidth - previewWidth) { + previewX = keyboardViewWidth - previewWidth; + statePosition = STATE_RIGHT; + } else { + statePosition = STATE_MIDDLE; + } + // The key preview is placed vertically above the top edge of the parent key with an + // arbitrary offset. + final int previewY = key.getY() - previewHeight + mParams.mPreviewOffset + + CoordinateUtils.y(originCoords); + + if (background != null) { + final int hasMoreKeys = (key.getMoreKeys() != null) ? STATE_HAS_MOREKEYS : STATE_NORMAL; + background.setState(KEY_PREVIEW_BACKGROUND_STATE_TABLE[statePosition][hasMoreKeys]); + } + ViewLayoutUtils.placeViewAt( + previewTextView, previewX, previewY, previewWidth, previewHeight); + previewTextView.setPivotX(previewWidth / 2.0f); + previewTextView.setPivotY(previewHeight); + } + + public void showKeyPreview(final Key key, final TextView previewTextView, + final boolean withAnimation) { + if (!withAnimation) { + previewTextView.setVisibility(View.VISIBLE); + mShowingKeyPreviewTextViews.put(key, previewTextView); + return; + } + + // Show preview with animation. + final Animator zoomIn = createZoomInAniation(key, previewTextView); + final Animator zoomOut = createZoomOutAnimation(key, previewTextView); + final KeyPreviewAnimations animation = new KeyPreviewAnimations(zoomIn, zoomOut); + previewTextView.setTag(animation); + animation.startZoomIn(); + } + + // TODO: Move these parameters to resources or preferences. + private static final float KEY_PREVIEW_START_ZOOM_IN_SCALE = 0.7f; + private static final float KEY_PREVIEW_END_ZOOM_IN_SCALE = 1.0f; + private static final float KEY_PREVIEW_END_ZOOM_OUT_SCALE = 0.7f; + private static final AccelerateInterpolator ACCELERATE_INTERPOLATOR = + new AccelerateInterpolator(); + private static final DecelerateInterpolator DECELERATE_INTERPOLATOR = + new DecelerateInterpolator(); + + private Animator createZoomInAniation(final Key key, final TextView previewTextView) { + final ObjectAnimator scaleXAnimation = ObjectAnimator.ofFloat( + previewTextView, View.SCALE_X, KEY_PREVIEW_START_ZOOM_IN_SCALE, + KEY_PREVIEW_END_ZOOM_IN_SCALE); + final ObjectAnimator scaleYAnimation = ObjectAnimator.ofFloat( + previewTextView, View.SCALE_Y, KEY_PREVIEW_START_ZOOM_IN_SCALE, + KEY_PREVIEW_END_ZOOM_IN_SCALE); + final AnimatorSet zoomInAnimation = new AnimatorSet(); + zoomInAnimation.play(scaleXAnimation).with(scaleYAnimation); + // TODO: Implement preference option to control key preview animation duration. + zoomInAnimation.setDuration(mParams.mZoomInDuration); + zoomInAnimation.setInterpolator(DECELERATE_INTERPOLATOR); + zoomInAnimation.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(final Animator animation) { + showKeyPreview(key, previewTextView, false /* withAnimation */); + } + }); + return zoomInAnimation; + } + + private Animator createZoomOutAnimation(final Key key, final TextView previewTextView) { + final ObjectAnimator scaleXAnimation = ObjectAnimator.ofFloat( + previewTextView, View.SCALE_X, KEY_PREVIEW_END_ZOOM_OUT_SCALE); + final ObjectAnimator scaleYAnimation = ObjectAnimator.ofFloat( + previewTextView, View.SCALE_Y, KEY_PREVIEW_END_ZOOM_OUT_SCALE); + final AnimatorSet zoomOutAnimation = new AnimatorSet(); + zoomOutAnimation.play(scaleXAnimation).with(scaleYAnimation); + // TODO: Implement preference option to control key preview animation duration. + final int zoomOutDuration = Math.min(mParams.mZoomOutDuration, mParams.getLingerTimeout()); + zoomOutAnimation.setDuration(zoomOutDuration); + zoomOutAnimation.setInterpolator(ACCELERATE_INTERPOLATOR); + zoomOutAnimation.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(final Animator animation) { + dismissKeyPreview(key, false /* withAnimation */); + } + }); + return zoomOutAnimation; + } + + private static class KeyPreviewAnimations extends AnimatorListenerAdapter { + private final Animator mZoomIn; + private final Animator mZoomOut; + + public KeyPreviewAnimations(final Animator zoomIn, final Animator zoomOut) { + mZoomIn = zoomIn; + mZoomOut = zoomOut; + } + + public void startZoomIn() { + mZoomIn.start(); + } + + public void startZoomOut() { + if (mZoomIn.isRunning()) { + mZoomIn.addListener(this); + return; + } + mZoomOut.start(); + } + + @Override + public void onAnimationEnd(final Animator animation) { + mZoomOut.start(); + } + } +} diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyPreviewDrawParams.java b/java/src/com/android/inputmethod/keyboard/internal/KeyPreviewDrawParams.java index 609d1a57f..ff493b198 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/KeyPreviewDrawParams.java +++ b/java/src/com/android/inputmethod/keyboard/internal/KeyPreviewDrawParams.java @@ -16,7 +16,22 @@ package com.android.inputmethod.keyboard.internal; +import android.content.res.TypedArray; +import android.view.View; + +import com.android.inputmethod.latin.R; + public final class KeyPreviewDrawParams { + // XML attributes of {@link MainKeyboardView}. + public final int mLayoutId; + public final int mPreviewOffset; + public final int mPreviewHeight; + // TODO: Move those parameters to preferences. + public final int mZoomInDuration; + public final int mZoomOutDuration; + private int mLingerTimeout; + private boolean mShowPopup = true; + // The graphical geometry of the key preview. // <-width-> // +-------+ ^ @@ -34,11 +49,72 @@ public final class KeyPreviewDrawParams { // paddings. To align the more keys keyboard panel's visible part with the visible part of // the background, we need to record the width and height of key preview that don't include // invisible paddings. - public int mPreviewVisibleWidth; - public int mPreviewVisibleHeight; + private int mVisibleWidth; + private int mVisibleHeight; // The key preview may have an arbitrary offset and its background that may have a bottom // padding. To align the more keys keyboard and the key preview we also need to record the // offset between the top edge of parent key and the bottom of the visible part of key // preview background. - public int mPreviewVisibleOffset; + private int mVisibleOffset; + + public KeyPreviewDrawParams(final TypedArray mainKeyboardViewAttr) { + mPreviewOffset = mainKeyboardViewAttr.getDimensionPixelOffset( + R.styleable.MainKeyboardView_keyPreviewOffset, 0); + mPreviewHeight = mainKeyboardViewAttr.getDimensionPixelSize( + R.styleable.MainKeyboardView_keyPreviewHeight, 0); + mLingerTimeout = mainKeyboardViewAttr.getInt( + R.styleable.MainKeyboardView_keyPreviewLingerTimeout, 0); + mLayoutId = mainKeyboardViewAttr.getResourceId( + R.styleable.MainKeyboardView_keyPreviewLayout, 0); + if (mLayoutId == 0) { + mShowPopup = false; + } + mZoomInDuration = mainKeyboardViewAttr.getInt( + R.styleable.MainKeyboardView_keyPreviewZoomInDuration, 0); + mZoomOutDuration = mainKeyboardViewAttr.getInt( + R.styleable.MainKeyboardView_keyPreviewZoomOutDuration, 0); + } + + public void setVisibleOffset(final int previewVisibleOffset) { + mVisibleOffset = previewVisibleOffset; + } + + public int getVisibleOffset() { + return mVisibleOffset; + } + + public void setGeometry(final View previewTextView) { + final int previewWidth = previewTextView.getMeasuredWidth(); + final int previewHeight = mPreviewHeight; + // The width and height of visible part of the key preview background. The content marker + // of the background 9-patch have to cover the visible part of the background. + mVisibleWidth = previewWidth - previewTextView.getPaddingLeft() + - previewTextView.getPaddingRight(); + mVisibleHeight = previewHeight - previewTextView.getPaddingTop() + - previewTextView.getPaddingBottom(); + // The distance between the top edge of the parent key and the bottom of the visible part + // of the key preview background. + setVisibleOffset(mPreviewOffset - previewTextView.getPaddingBottom()); + } + + public int getVisibleWidth() { + return mVisibleWidth; + } + + public int getVisibleHeight() { + return mVisibleHeight; + } + + public void setPopupEnabled(final boolean enabled, final int lingerTimeout) { + mShowPopup = enabled; + mLingerTimeout = lingerTimeout; + } + + public boolean isPopupEnabled() { + return mShowPopup; + } + + public int getLingerTimeout() { + return mLingerTimeout; + } } diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java b/java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java index c51941d32..2925a4b76 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java +++ b/java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java @@ -56,10 +56,9 @@ public final class KeySpecParser { return keySpec.startsWith(KeyboardIconsSet.PREFIX_ICON); } - private static boolean hasCode(final String keySpec) { - final int end = indexOfLabelEnd(keySpec, 0); - if (end > 0 && end + 1 < keySpec.length() && keySpec.startsWith( - KeyboardCodesSet.PREFIX_CODE, end + 1)) { + private static boolean hasCode(final String keySpec, final int labelEnd) { + if (labelEnd > 0 && labelEnd + 1 < keySpec.length() + && keySpec.startsWith(KeyboardCodesSet.PREFIX_CODE, labelEnd + 1)) { return true; } return false; @@ -84,16 +83,20 @@ public final class KeySpecParser { return sb.toString(); } - private static int indexOfLabelEnd(final String keySpec, final int start) { - if (keySpec.indexOf(BACKSLASH, start) < 0) { - final int end = keySpec.indexOf(VERTICAL_BAR, start); - if (end == 0) { - throw new KeySpecParserError(VERTICAL_BAR + " at " + start + ": " + keySpec); + private static int indexOfLabelEnd(final String keySpec) { + final int length = keySpec.length(); + if (keySpec.indexOf(BACKSLASH) < 0) { + final int labelEnd = keySpec.indexOf(VERTICAL_BAR); + if (labelEnd == 0) { + if (length == 1) { + // Treat a sole vertical bar as a special case of key label. + return -1; + } + throw new KeySpecParserError("Empty label"); } - return end; + return labelEnd; } - final int length = keySpec.length(); - for (int pos = start; pos < length; pos++) { + for (int pos = 0; pos < length; pos++) { final char c = keySpec.charAt(pos); if (c == BACKSLASH && pos + 1 < length) { // Skip escape char @@ -105,35 +108,55 @@ public final class KeySpecParser { return -1; } + private static String getBeforeLabelEnd(final String keySpec, final int labelEnd) { + return (labelEnd < 0) ? keySpec : keySpec.substring(0, labelEnd); + } + + private static String getAfterLabelEnd(final String keySpec, final int labelEnd) { + return keySpec.substring(labelEnd + /* VERTICAL_BAR */1); + } + + private static void checkDoubleLabelEnd(final String keySpec, final int labelEnd) { + if (indexOfLabelEnd(getAfterLabelEnd(keySpec, labelEnd)) < 0) { + return; + } + throw new KeySpecParserError("Multiple " + VERTICAL_BAR + ": " + keySpec); + } + public static String getLabel(final String keySpec) { + if (keySpec == null) { + // TODO: Throw {@link KeySpecParserError} once Key.keyLabel attribute becomes mandatory. + return null; + } if (hasIcon(keySpec)) { return null; } - final int end = indexOfLabelEnd(keySpec, 0); - final String label = (end > 0) ? parseEscape(keySpec.substring(0, end)) - : parseEscape(keySpec); + final int labelEnd = indexOfLabelEnd(keySpec); + final String label = parseEscape(getBeforeLabelEnd(keySpec, labelEnd)); if (label.isEmpty()) { throw new KeySpecParserError("Empty label: " + keySpec); } return label; } - private static String getOutputTextInternal(final String keySpec) { - final int end = indexOfLabelEnd(keySpec, 0); - if (end <= 0) { + private static String getOutputTextInternal(final String keySpec, final int labelEnd) { + if (labelEnd <= 0) { return null; } - if (indexOfLabelEnd(keySpec, end + 1) >= 0) { - throw new KeySpecParserError("Multiple " + VERTICAL_BAR + ": " + keySpec); - } - return parseEscape(keySpec.substring(end + /* VERTICAL_BAR */1)); + checkDoubleLabelEnd(keySpec, labelEnd); + return parseEscape(getAfterLabelEnd(keySpec, labelEnd)); } public static String getOutputText(final String keySpec) { - if (hasCode(keySpec)) { + if (keySpec == null) { + // TODO: Throw {@link KeySpecParserError} once Key.keyLabel attribute becomes mandatory. return null; } - final String outputText = getOutputTextInternal(keySpec); + final int labelEnd = indexOfLabelEnd(keySpec); + if (hasCode(keySpec, labelEnd)) { + return null; + } + final String outputText = getOutputTextInternal(keySpec, labelEnd); if (outputText != null) { if (StringUtils.codePointCount(outputText) == 1) { // If output text is one code point, it should be treated as a code. @@ -154,14 +177,16 @@ public final class KeySpecParser { } public static int getCode(final String keySpec, final KeyboardCodesSet codesSet) { - if (hasCode(keySpec)) { - final int end = indexOfLabelEnd(keySpec, 0); - if (indexOfLabelEnd(keySpec, end + 1) >= 0) { - throw new KeySpecParserError("Multiple " + VERTICAL_BAR + ": " + keySpec); - } - return parseCode(keySpec.substring(end + 1), codesSet, CODE_UNSPECIFIED); + if (keySpec == null) { + // TODO: Throw {@link KeySpecParserError} once Key.keyLabel attribute becomes mandatory. + return CODE_UNSPECIFIED; } - final String outputText = getOutputTextInternal(keySpec); + final int labelEnd = indexOfLabelEnd(keySpec); + if (hasCode(keySpec, labelEnd)) { + checkDoubleLabelEnd(keySpec, labelEnd); + return parseCode(getAfterLabelEnd(keySpec, labelEnd), codesSet, CODE_UNSPECIFIED); + } + final String outputText = getOutputTextInternal(keySpec, labelEnd); if (outputText != null) { // If output text is one code point, it should be treated as a code. // See {@link #getOutputText(String)}. @@ -171,35 +196,40 @@ public final class KeySpecParser { return CODE_OUTPUT_TEXT; } final String label = getLabel(keySpec); - // Code is automatically generated for one letter label. - if (StringUtils.codePointCount(label) == 1) { - return label.codePointAt(0); + if (label == null) { + throw new KeySpecParserError("Empty label: " + keySpec); } - return CODE_OUTPUT_TEXT; + // Code is automatically generated for one letter label. + return (StringUtils.codePointCount(label) == 1) ? label.codePointAt(0) : CODE_OUTPUT_TEXT; } + // TODO: Make this method private once Key.code attribute is removed. public static int parseCode(final String text, final KeyboardCodesSet codesSet, final int defCode) { - if (text == null) return defCode; + if (text == null) { + return defCode; + } if (text.startsWith(KeyboardCodesSet.PREFIX_CODE)) { return codesSet.getCode(text.substring(KeyboardCodesSet.PREFIX_CODE.length())); - } else if (text.startsWith(PREFIX_HEX)) { + } + if (text.startsWith(PREFIX_HEX)) { return Integer.parseInt(text.substring(PREFIX_HEX.length()), 16); - } else { - return Integer.parseInt(text); } + return Integer.parseInt(text); } public static int getIconId(final String keySpec) { - if (keySpec != null && hasIcon(keySpec)) { - final int end = keySpec.indexOf( - VERTICAL_BAR, KeyboardIconsSet.PREFIX_ICON.length()); - final String name = (end < 0) - ? keySpec.substring(KeyboardIconsSet.PREFIX_ICON.length()) - : keySpec.substring(KeyboardIconsSet.PREFIX_ICON.length(), end); - return KeyboardIconsSet.getIconId(name); - } - return KeyboardIconsSet.ICON_UNDEFINED; + if (keySpec == null) { + // TODO: Throw {@link KeySpecParserError} once Key.keyLabel attribute becomes mandatory. + return KeyboardIconsSet.ICON_UNDEFINED; + } + if (!hasIcon(keySpec)) { + return KeyboardIconsSet.ICON_UNDEFINED; + } + final int labelEnd = indexOfLabelEnd(keySpec); + final String iconName = getBeforeLabelEnd(keySpec, labelEnd) + .substring(KeyboardIconsSet.PREFIX_ICON.length()); + return KeyboardIconsSet.getIconId(iconName); } @SuppressWarnings("serial") diff --git a/java/src/com/android/inputmethod/keyboard/internal/MoreKeySpec.java b/java/src/com/android/inputmethod/keyboard/internal/MoreKeySpec.java index d3bc0c2b2..0551e9e98 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/MoreKeySpec.java +++ b/java/src/com/android/inputmethod/keyboard/internal/MoreKeySpec.java @@ -46,6 +46,9 @@ public final class MoreKeySpec { public MoreKeySpec(final String moreKeySpec, boolean needsToUpperCase, final Locale locale, final KeyboardCodesSet codesSet) { + if (TextUtils.isEmpty(moreKeySpec)) { + throw new KeySpecParser.KeySpecParserError("Empty more key spec"); + } mLabel = StringUtils.toUpperCaseOfStringForLocale( KeySpecParser.getLabel(moreKeySpec), needsToUpperCase, locale); final int code = StringUtils.toUpperCaseOfCodeForLocale( diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionary.java b/java/src/com/android/inputmethod/latin/BinaryDictionary.java index 00eb57c9f..b20bcd1f9 100644 --- a/java/src/com/android/inputmethod/latin/BinaryDictionary.java +++ b/java/src/com/android/inputmethod/latin/BinaryDictionary.java @@ -22,7 +22,10 @@ import android.util.SparseArray; import com.android.inputmethod.annotations.UsedForTesting; import com.android.inputmethod.keyboard.ProximityInfo; import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; -import com.android.inputmethod.latin.makedict.Word; +import com.android.inputmethod.latin.makedict.DictionaryHeader; +import com.android.inputmethod.latin.makedict.FormatSpec; +import com.android.inputmethod.latin.makedict.FusionDictionary.DictionaryOptions; +import com.android.inputmethod.latin.makedict.UnsupportedFormatException; import com.android.inputmethod.latin.settings.NativeSuggestOptions; import com.android.inputmethod.latin.utils.CollectionUtils; import com.android.inputmethod.latin.utils.JniUtils; @@ -33,6 +36,7 @@ import com.android.inputmethod.latin.utils.WordProperty; import java.io.File; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.Locale; import java.util.Map; @@ -135,9 +139,12 @@ public final class BinaryDictionary extends Dictionary { } private static native boolean createEmptyDictFileNative(String filePath, long dictVersion, - String[] attributeKeyStringArray, String[] attributeValueStringArray); + String locale, String[] attributeKeyStringArray, String[] attributeValueStringArray); private static native long openNative(String sourceDir, long dictOffset, long dictSize, boolean isUpdatable); + private static native void getHeaderInfoNative(long dict, int[] outHeaderSize, + int[] outFormatVersion, ArrayList<int[]> outAttributeKeys, + ArrayList<int[]> outAttributeValues); private static native void flushNative(long dict, String filePath); private static native boolean needsToRunGCNative(long dict, boolean mindsBlockByGC); private static native void flushWithGCNative(long dict, String filePath); @@ -171,9 +178,8 @@ public final class BinaryDictionary extends Dictionary { private static native int setCurrentTimeForTestNative(int currentTime); private static native String getPropertyNative(long dict, String query); - @UsedForTesting public static boolean createEmptyDictFile(final String filePath, final long dictVersion, - final Map<String, String> attributeMap) { + final Locale locale, final Map<String, String> attributeMap) { final String[] keyArray = new String[attributeMap.size()]; final String[] valueArray = new String[attributeMap.size()]; int index = 0; @@ -182,7 +188,8 @@ public final class BinaryDictionary extends Dictionary { valueArray[index] = attributeMap.get(key); index++; } - return createEmptyDictFileNative(filePath, dictVersion, keyArray, valueArray); + return createEmptyDictFileNative(filePath, dictVersion, locale.toString(), keyArray, + valueArray); } // TODO: Move native dict into session @@ -191,6 +198,33 @@ public final class BinaryDictionary extends Dictionary { mNativeDict = openNative(path, startOffset, length, isUpdatable); } + @UsedForTesting + public DictionaryHeader getHeader() throws UnsupportedFormatException { + if (mNativeDict == 0) { + return null; + } + final int[] outHeaderSize = new int[1]; + final int[] outFormatVersion = new int[1]; + final ArrayList<int[]> outAttributeKeys = CollectionUtils.newArrayList(); + final ArrayList<int[]> outAttributeValues = CollectionUtils.newArrayList(); + getHeaderInfoNative(mNativeDict, outHeaderSize, outFormatVersion, outAttributeKeys, + outAttributeValues); + final HashMap<String, String> attributes = new HashMap<String, String>(); + for (int i = 0; i < outAttributeKeys.size(); i++) { + final String attributeKey = StringUtils.getStringFromNullTerminatedCodePointArray( + outAttributeKeys.get(i)); + final String attributeValue = StringUtils.getStringFromNullTerminatedCodePointArray( + outAttributeValues.get(i)); + attributes.put(attributeKey, attributeValue); + } + final boolean hasHistoricalInfo = + attributes.get(DictionaryHeader.HAS_HISTORICAL_INFO_KEY).equals( + DictionaryHeader.ATTRIBUTE_VALUE_TRUE); + return new DictionaryHeader(outHeaderSize[0], new DictionaryOptions(attributes), + new FormatSpec.FormatOptions(outFormatVersion[0], hasHistoricalInfo)); + } + + @Override public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer, final String prevWord, final ProximityInfo proximityInfo, @@ -308,7 +342,6 @@ public final class BinaryDictionary extends Dictionary { return getBigramProbabilityNative(mNativeDict, codePoints0, codePoints1); } - @UsedForTesting public WordProperty getWordProperty(final String word) { if (TextUtils.isEmpty(word)) { return null; @@ -348,16 +381,10 @@ public final class BinaryDictionary extends Dictionary { * Method to iterate all words in the dictionary for makedict. * If token is 0, this method newly starts iterating the dictionary. */ - @UsedForTesting public GetNextWordPropertyResult getNextWordProperty(final int token) { final int[] codePoints = new int[MAX_WORD_LENGTH]; final int nextToken = getNextWordNative(mNativeDict, token, codePoints); - int len = 0; - // codePoints is null-terminated if its length is shorter than the array length. - while (len < MAX_WORD_LENGTH && codePoints[len] != 0) { - ++len; - } - final String word = new String(mOutputCodePoints, 0, len); + final String word = StringUtils.getStringFromNullTerminatedCodePointArray(codePoints); return new GetNextWordPropertyResult(getWordProperty(word), nextToken); } diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java b/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java index 7e97802e1..a7008379f 100644 --- a/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java +++ b/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java @@ -22,8 +22,8 @@ import android.content.res.AssetFileDescriptor; import android.util.Log; import com.android.inputmethod.latin.makedict.DictDecoder; +import com.android.inputmethod.latin.makedict.DictionaryHeader; import com.android.inputmethod.latin.makedict.FormatSpec; -import com.android.inputmethod.latin.makedict.FormatSpec.FileHeader; import com.android.inputmethod.latin.makedict.UnsupportedFormatException; import com.android.inputmethod.latin.utils.CollectionUtils; import com.android.inputmethod.latin.utils.DictionaryInfoUtils; @@ -230,7 +230,7 @@ final public class BinaryDictionaryGetter { try { // Read the version of the file final DictDecoder dictDecoder = FormatSpec.getDictDecoder(f); - final FileHeader header = dictDecoder.readHeader(); + final DictionaryHeader header = dictDecoder.readHeader(); final String version = header.mDictionaryOptions.mAttributes.get(VERSION_KEY); if (null == version) { diff --git a/java/src/com/android/inputmethod/latin/DictionaryDumpBroadcastReceiver.java b/java/src/com/android/inputmethod/latin/DictionaryDumpBroadcastReceiver.java new file mode 100644 index 000000000..ee2fdc6c7 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/DictionaryDumpBroadcastReceiver.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.latin; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.util.Log; + +public class DictionaryDumpBroadcastReceiver extends BroadcastReceiver { + private static final String TAG = DictionaryDumpBroadcastReceiver.class.getSimpleName(); + + private static final String DOMAIN = "com.android.inputmethod.latin"; + public static final String DICTIONARY_DUMP_INTENT_ACTION = DOMAIN + ".DICT_DUMP"; + public static final String DICTIONARY_NAME_KEY = "dictName"; + + final LatinIME mLatinIme; + + public DictionaryDumpBroadcastReceiver(final LatinIME latinIme) { + mLatinIme = latinIme; + } + + @Override + public void onReceive(Context context, Intent intent) { + final String action = intent.getAction(); + if (action.equals(DICTIONARY_DUMP_INTENT_ACTION)) { + final String dictName = intent.getStringExtra(DICTIONARY_NAME_KEY); + if (dictName == null) { + Log.e(TAG, "Received dictionary dump intent action " + + "but the dictionary name is not set."); + return; + } + mLatinIme.dumpDictionaryForDebug(dictName); + } + } +} diff --git a/java/src/com/android/inputmethod/latin/DictionaryFacilitatorForSuggest.java b/java/src/com/android/inputmethod/latin/DictionaryFacilitatorForSuggest.java index 8b02984e0..e68c6b771 100644 --- a/java/src/com/android/inputmethod/latin/DictionaryFacilitatorForSuggest.java +++ b/java/src/com/android/inputmethod/latin/DictionaryFacilitatorForSuggest.java @@ -534,4 +534,25 @@ public class DictionaryFacilitatorForSuggest { mPersonalizationDictionary.addMultipleDictionaryEntriesToDictionary(languageModelParams, callback); } + + public void dumpDictionaryForDebug(final String dictName) { + final ExpandableBinaryDictionary dictToDump; + if (dictName.equals(Dictionary.TYPE_CONTACTS)) { + dictToDump = mContactsDictionary; + } else if (dictName.equals(Dictionary.TYPE_USER)) { + dictToDump = mUserDictionary; + } else if (dictName.equals(Dictionary.TYPE_USER_HISTORY)) { + dictToDump = mUserHistoryDictionary; + } else if (dictName.equals(Dictionary.TYPE_PERSONALIZATION)) { + dictToDump = mPersonalizationDictionary; + } else { + dictToDump = null; + } + if (dictToDump == null) { + Log.e(TAG, "Cannot dump " + dictName + ". " + + "The dictionary is not being used for suggestion or cannot be dumped."); + return; + } + dictToDump.dumpAllWordsForDebug(); + } } diff --git a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java index 4dee84a7b..565d6a1f9 100644 --- a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java +++ b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java @@ -21,6 +21,7 @@ import android.util.Log; import com.android.inputmethod.annotations.UsedForTesting; import com.android.inputmethod.keyboard.ProximityInfo; +import com.android.inputmethod.latin.makedict.DictionaryHeader; import com.android.inputmethod.latin.makedict.FormatSpec; import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; import com.android.inputmethod.latin.utils.AsyncResultHolder; @@ -28,6 +29,7 @@ import com.android.inputmethod.latin.utils.CollectionUtils; import com.android.inputmethod.latin.utils.FileUtils; import com.android.inputmethod.latin.utils.LanguageModelParam; import com.android.inputmethod.latin.utils.PrioritizedSerialExecutor; +import com.android.inputmethod.latin.utils.WordProperty; import java.io.File; import java.util.ArrayList; @@ -267,9 +269,9 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { protected Map<String, String> getHeaderAttributeMap() { HashMap<String, String> attributeMap = new HashMap<String, String>(); - attributeMap.put(FormatSpec.FileHeader.DICTIONARY_ID_KEY, mDictName); - attributeMap.put(FormatSpec.FileHeader.DICTIONARY_LOCALE_KEY, mLocale.toString()); - attributeMap.put(FormatSpec.FileHeader.DICTIONARY_VERSION_KEY, + attributeMap.put(DictionaryHeader.DICTIONARY_ID_KEY, mDictName); + attributeMap.put(DictionaryHeader.DICTIONARY_LOCALE_KEY, mLocale.toString()); + attributeMap.put(DictionaryHeader.DICTIONARY_VERSION_KEY, String.valueOf(TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()))); return attributeMap; } @@ -287,10 +289,10 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { Log.e(TAG, "Can't remove a file: " + file.getName()); } BinaryDictionary.createEmptyDictFile(file.getAbsolutePath(), - DICTIONARY_FORMAT_VERSION, getHeaderAttributeMap()); + DICTIONARY_FORMAT_VERSION, mLocale, getHeaderAttributeMap()); mBinaryDictionary = new BinaryDictionary( file.getAbsolutePath(), 0 /* offset */, file.length(), - true /* useFullEditDistance */, null, mDictType, mIsUpdatable); + true /* useFullEditDistance */, mLocale, mDictType, mIsUpdatable); } else { mDictionaryWriter.clear(); } @@ -592,7 +594,7 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { Log.e(TAG, "Can't remove a file: " + file.getName()); } BinaryDictionary.createEmptyDictFile(file.getAbsolutePath(), - DICTIONARY_FORMAT_VERSION, getHeaderAttributeMap()); + DICTIONARY_FORMAT_VERSION, mLocale, getHeaderAttributeMap()); } else { if (mBinaryDictionary.needsToRunGC(false /* mindsBlockByGC */)) { mBinaryDictionary.flushWithGC(); @@ -778,16 +780,24 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { } @UsedForTesting - protected void runAfterGcForDebug(final Runnable r) { - getExecutor(mDictName).executePrioritized(new Runnable() { + public void dumpAllWordsForDebug() { + reloadDictionaryIfRequired(); + getExecutor(mDictName).execute(new Runnable() { @Override public void run() { - try { - mBinaryDictionary.flushWithGC(); - r.run(); - } finally { - mDictNameDictionaryUpdateController.mProcessingLargeTask.set(false); - } + Log.d(TAG, "dictionary=" + mDictName); + int token = 0; + do { + final BinaryDictionary.GetNextWordPropertyResult result = + mBinaryDictionary.getNextWordProperty(token); + final WordProperty wordProperty = result.mWordProperty; + if (wordProperty == null) { + Log.d(TAG, " dictionary is empty."); + break; + } + Log.d(TAG, wordProperty.toString()); + token = result.mNextToken; + } while (token != 0); } }); } diff --git a/java/src/com/android/inputmethod/latin/InputPointers.java b/java/src/com/android/inputmethod/latin/InputPointers.java index c3bcf3785..47bc6b078 100644 --- a/java/src/com/android/inputmethod/latin/InputPointers.java +++ b/java/src/com/android/inputmethod/latin/InputPointers.java @@ -17,6 +17,7 @@ package com.android.inputmethod.latin; import android.util.Log; +import android.util.SparseIntArray; import com.android.inputmethod.annotations.UsedForTesting; import com.android.inputmethod.latin.utils.ResizableIntArray; @@ -160,15 +161,21 @@ public final class InputPointers { private boolean isValidTimeStamps() { final int[] times = mTimes.getPrimitiveArray(); + final int[] pointerIds = mPointerIds.getPrimitiveArray(); + final SparseIntArray lastTimeOfPointers = new SparseIntArray(); final int size = getPointerSize(); - for (int i = 1; i < size; ++i) { - if (times[i] < times[i - 1]) { + for (int i = 0; i < size; ++i) { + final int pointerId = pointerIds[i]; + final int time = times[i]; + final int lastTime = lastTimeOfPointers.get(pointerId, time); + if (time < lastTime) { // dump for (int j = 0; j < size; ++j) { Log.d(TAG, "--- (" + j + ") " + times[j]); } return false; } + lastTimeOfPointers.put(pointerId, time); } return true; } diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java index 6517ef29d..e55c08dae 100644 --- a/java/src/com/android/inputmethod/latin/LatinIME.java +++ b/java/src/com/android/inputmethod/latin/LatinIME.java @@ -67,7 +67,6 @@ import com.android.inputmethod.latin.Suggest.OnGetSuggestedWordsCallback; import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; import com.android.inputmethod.latin.define.ProductionFlag; import com.android.inputmethod.latin.inputlogic.InputLogic; -import com.android.inputmethod.latin.inputlogic.SpaceState; import com.android.inputmethod.latin.personalization.DictionaryDecayBroadcastReciever; import com.android.inputmethod.latin.personalization.PersonalizationDictionarySessionRegistrar; import com.android.inputmethod.latin.personalization.PersonalizationHelper; @@ -75,6 +74,7 @@ import com.android.inputmethod.latin.settings.Settings; import com.android.inputmethod.latin.settings.SettingsActivity; import com.android.inputmethod.latin.settings.SettingsValues; import com.android.inputmethod.latin.suggestions.SuggestionStripView; +import com.android.inputmethod.latin.suggestions.SuggestionStripViewAccessor; import com.android.inputmethod.latin.utils.ApplicationUtils; import com.android.inputmethod.latin.utils.CapsModeUtils; import com.android.inputmethod.latin.utils.CompletionInfoUtils; @@ -82,7 +82,6 @@ import com.android.inputmethod.latin.utils.CoordinateUtils; import com.android.inputmethod.latin.utils.ImportantNoticeUtils; import com.android.inputmethod.latin.utils.IntentUtils; import com.android.inputmethod.latin.utils.JniUtils; -import com.android.inputmethod.latin.utils.LatinImeLoggerUtils; import com.android.inputmethod.latin.utils.LeakGuardHandlerWrapper; import com.android.inputmethod.latin.utils.SubtypeLocaleUtils; import com.android.inputmethod.research.ResearchLogger; @@ -97,7 +96,7 @@ import java.util.concurrent.TimeUnit; * Input method implementation for Qwerty'ish keyboard. */ public class LatinIME extends InputMethodService implements KeyboardActionListener, - SuggestionStripView.Listener, + SuggestionStripView.Listener, SuggestionStripViewAccessor, DictionaryFacilitatorForSuggest.DictionaryInitializationListener { private static final String TAG = LatinIME.class.getSimpleName(); private static final boolean TRACE = false; @@ -116,13 +115,15 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen private static final String SCHEME_PACKAGE = "package"; private final Settings mSettings; - private final InputLogic mInputLogic = new InputLogic(this); + private final InputLogic mInputLogic = new InputLogic(this /* LatinIME */, + this /* SuggestionStripViewAccessor */); private View mExtractArea; private View mKeyPreviewBackingView; private SuggestionStripView mSuggestionStripView; - private CompletionInfo[] mApplicationSpecifiedCompletions; + // TODO[IL]: remove this member completely. + public CompletionInfo[] mApplicationSpecifiedCompletions; private RichInputMethodManager mRichImm; @UsedForTesting final KeyboardSwitcher mKeyboardSwitcher; @@ -133,6 +134,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen private BroadcastReceiver mDictionaryPackInstallReceiver = new DictionaryPackInstallBroadcastReceiver(this); + private BroadcastReceiver mDictionaryDumpBroadcastReceiver = + new DictionaryDumpBroadcastReceiver(this); + private AlertDialog mOptionsDialog; private final boolean mIsHardwareAcceleratedDrawingEnabled; @@ -487,6 +491,10 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen newDictFilter.addAction(DictionaryPackConstants.NEW_DICTIONARY_INTENT_ACTION); registerReceiver(mDictionaryPackInstallReceiver, newDictFilter); + final IntentFilter dictDumpFilter = new IntentFilter(); + dictDumpFilter.addAction(DictionaryDumpBroadcastReceiver.DICTIONARY_DUMP_INTENT_ACTION); + registerReceiver(mDictionaryDumpBroadcastReceiver, dictDumpFilter); + DictionaryDecayBroadcastReciever.setUpIntervalAlarmForDictionaryDecaying(this); } @@ -1298,13 +1306,10 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // Nothing to do so far. } - // TODO[IL]: Move this to InputLogic and make it private - // Outside LatinIME, only used by the test suite. + // TODO: remove this, read this directly from mInputLogic or something in the tests @UsedForTesting public boolean isShowingPunctuationList() { - if (mInputLogic.mSuggestedWords == null) return false; - return mSettings.getCurrent().mSpacingAndPunctuations.mSuggestPuncList - == mInputLogic.mSuggestedWords; + return mInputLogic.isShowingPunctuationList(mSettings.getCurrent()); } // TODO[IL]: Define a clear interface for this @@ -1323,6 +1328,17 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen return currentSettings.isSuggestionsRequested(); } + @Override + public boolean hasSuggestionStripView() { + return null != mSuggestionStripView; + } + + @Override + public boolean isShowingAddToDictionaryHint() { + return hasSuggestionStripView() && mSuggestionStripView.isShowingAddToDictionaryHint(); + } + + @Override public void dismissAddToDictionaryHint() { if (null != mSuggestionStripView) { mSuggestionStripView.dismissAddToDictionaryHint(); @@ -1392,8 +1408,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // the "add to dictionary" hint, we need to revert to suggestions - although it is unclear // how we can come here if it's displayed. if (suggestedWords.size() > 1 || typedWord.length() <= 1 - || null == mSuggestionStripView - || mSuggestionStripView.isShowingAddToDictionaryHint()) { + || null == mSuggestionStripView || isShowingAddToDictionaryHint()) { return suggestedWords; } else { final SuggestedWords punctuationList = @@ -1411,7 +1426,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } } - // TODO[IL]: Define a clean interface for this + @Override public void showSuggestionStrip(final SuggestedWords sourceSuggestedWords) { final SuggestedWords suggestedWords = sourceSuggestedWords.isEmpty() ? SuggestedWords.EMPTY : sourceSuggestedWords; @@ -1439,99 +1454,20 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // interface @Override public void pickSuggestionManually(final int index, final SuggestedWordInfo suggestionInfo) { - final SuggestedWords suggestedWords = mInputLogic.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. - // So, LatinImeLogger logs "" as a user's input. - LatinImeLogger.logOnManualSuggestion("", suggestion, index, suggestedWords); - // Rely on onCodeInput to do the complicated swapping/stripping logic consistently. - final int primaryCode = suggestion.charAt(0); - onCodeInput(primaryCode, - Constants.SUGGESTION_STRIP_COORDINATE, Constants.SUGGESTION_STRIP_COORDINATE); - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - ResearchLogger.latinIME_punctuationSuggestion(index, suggestion, - false /* isBatchMode */, suggestedWords.mIsPrediction); - } - return; - } - - mInputLogic.mConnection.beginBatchEdit(); - final SettingsValues currentSettings = mSettings.getCurrent(); - if (SpaceState.PHANTOM == mInputLogic.mSpaceState && suggestion.length() > 0 - // In the batch input mode, a manually picked suggested word should just replace - // the current batch input text and there is no need for a phantom space. - && !mInputLogic.mWordComposer.isBatchMode()) { - final int firstChar = Character.codePointAt(suggestion, 0); - if (!currentSettings.isWordSeparator(firstChar) - || currentSettings.isUsuallyPrecededBySpace(firstChar)) { - mInputLogic.promotePhantomSpace(currentSettings); - } - } - - if (currentSettings.isApplicationSpecifiedCompletionsOn() - && mApplicationSpecifiedCompletions != null - && index >= 0 && index < mApplicationSpecifiedCompletions.length) { - mInputLogic.mSuggestedWords = SuggestedWords.EMPTY; - if (mSuggestionStripView != null) { - mSuggestionStripView.clear(); - } - mKeyboardSwitcher.updateShiftState(); - mInputLogic.resetComposingState(true /* alsoResetLastComposedWord */); - final CompletionInfo completionInfo = mApplicationSpecifiedCompletions[index]; - mInputLogic.mConnection.commitCompletion(completionInfo); - mInputLogic.mConnection.endBatchEdit(); - return; - } - - // We need to log before we commit, because the word composer will store away the user - // typed word. - final String replacedWord = mInputLogic.mWordComposer.getTypedWord(); - LatinImeLogger.logOnManualSuggestion(replacedWord, suggestion, index, suggestedWords); - mInputLogic.commitChosenWord(currentSettings, suggestion, - LastComposedWord.COMMIT_TYPE_MANUAL_PICK, LastComposedWord.NOT_A_SEPARATOR); - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - ResearchLogger.latinIME_pickSuggestionManually(replacedWord, index, suggestion, - mInputLogic.mWordComposer.isBatchMode(), suggestionInfo.mScore, - suggestionInfo.mKind, suggestionInfo.mSourceDict.mDictType); - } - mInputLogic.mConnection.endBatchEdit(); - // Don't allow cancellation of manual pick - mInputLogic.mLastComposedWord.deactivate(); - // Space state must be updated before calling updateShiftState - mInputLogic.mSpaceState = SpaceState.PHANTOM; - mKeyboardSwitcher.updateShiftState(); + mInputLogic.onPickSuggestionManually(mSettings.getCurrent(), index, suggestionInfo, + mHandler, mKeyboardSwitcher); + } - // We should show the "Touch again to save" hint if the user pressed the first entry - // 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 Suggest suggest = mInputLogic.mSuggest; - final boolean showingAddToDictionaryHint = - (SuggestedWordInfo.KIND_TYPED == suggestionInfo.mKind - || SuggestedWordInfo.KIND_OOV_CORRECTION == suggestionInfo.mKind) - && suggest != null - // If the suggestion is not in the dictionary, the hint should be shown. - && !suggest.mDictionaryFacilitator.isValidWord(suggestion, - true /* ignoreCase */); - - if (currentSettings.mIsInternal) { - LatinImeLoggerUtils.onSeparator((char)Constants.CODE_SPACE, - Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE); - } - if (showingAddToDictionaryHint - && suggest.mDictionaryFacilitator.isUserDictionaryEnabled()) { - mSuggestionStripView.showAddToDictionaryHint(suggestion); - } else { - // If we're not showing the "Touch again to save", then update the suggestion strip. - mHandler.postUpdateSuggestionStrip(); - } + @Override + public void showAddToDictionaryHint(final String word) { + if (null == mSuggestionStripView) return; + mSuggestionStripView.showAddToDictionaryHint(word); } // TODO[IL]: Define a clean interface for this // This will show either an empty suggestion strip (if prediction is enabled) or // punctuation suggestions (if it's disabled). + @Override public void setNeutralSuggestionStrip() { final SettingsValues currentSettings = mSettings.getCurrent(); if (currentSettings.mBigramPredictionEnabled) { @@ -1758,6 +1694,13 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen resetSuggest(new Suggest(locale, dictionaryFacilitator)); } + public void dumpDictionaryForDebug(final String dictName) { + if (mInputLogic.mSuggest == null) { + initSuggest(); + } + mInputLogic.mSuggest.mDictionaryFacilitator.dumpDictionaryForDebug(dictName); + } + public void debugDumpStateAndCrashWithException(final String context) { final SettingsValues settingsValues = mSettings.getCurrent(); final StringBuilder s = new StringBuilder(settingsValues.toString()); diff --git a/java/src/com/android/inputmethod/latin/debug/ExternalDictionaryGetterForDebug.java b/java/src/com/android/inputmethod/latin/debug/ExternalDictionaryGetterForDebug.java index 028f78a87..800f56597 100644 --- a/java/src/com/android/inputmethod/latin/debug/ExternalDictionaryGetterForDebug.java +++ b/java/src/com/android/inputmethod/latin/debug/ExternalDictionaryGetterForDebug.java @@ -26,7 +26,7 @@ import android.os.Environment; import com.android.inputmethod.latin.BinaryDictionaryFileDumper; import com.android.inputmethod.latin.BinaryDictionaryGetter; import com.android.inputmethod.latin.R; -import com.android.inputmethod.latin.makedict.FormatSpec.FileHeader; +import com.android.inputmethod.latin.makedict.DictionaryHeader; import com.android.inputmethod.latin.utils.CollectionUtils; import com.android.inputmethod.latin.utils.DictionaryInfoUtils; import com.android.inputmethod.latin.utils.LocaleUtils; @@ -51,7 +51,7 @@ public class ExternalDictionaryGetterForDebug { final File[] files = new File(SOURCE_FOLDER).listFiles(); final ArrayList<String> eligibleList = CollectionUtils.newArrayList(); for (File f : files) { - final FileHeader header = DictionaryInfoUtils.getDictionaryFileHeaderOrNull(f); + final DictionaryHeader header = DictionaryInfoUtils.getDictionaryFileHeaderOrNull(f); if (null == header) continue; eligibleList.add(f.getName()); } @@ -99,7 +99,7 @@ public class ExternalDictionaryGetterForDebug { public static void askInstallFile(final Context context, final String dirPath, final String fileName, final Runnable completeRunnable) { final File file = new File(dirPath, fileName.toString()); - final FileHeader header = DictionaryInfoUtils.getDictionaryFileHeaderOrNull(file); + final DictionaryHeader header = DictionaryInfoUtils.getDictionaryFileHeaderOrNull(file); final StringBuilder message = new StringBuilder(); final String locale = header.getLocaleString(); for (String key : header.mDictionaryOptions.mAttributes.keySet()) { @@ -143,7 +143,7 @@ public class ExternalDictionaryGetterForDebug { } private static void installFile(final Context context, final File file, - final FileHeader header) { + final DictionaryHeader header) { BufferedOutputStream outputStream = null; File tempFile = null; try { diff --git a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java index f53183f37..3ecf5f0fb 100644 --- a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java +++ b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java @@ -23,6 +23,7 @@ import android.text.style.SuggestionSpan; import android.util.Log; import android.view.KeyCharacterMap; import android.view.KeyEvent; +import android.view.inputmethod.CompletionInfo; import android.view.inputmethod.CorrectionInfo; import android.view.inputmethod.EditorInfo; @@ -44,6 +45,7 @@ import com.android.inputmethod.latin.WordComposer; import com.android.inputmethod.latin.define.ProductionFlag; import com.android.inputmethod.latin.settings.SettingsValues; import com.android.inputmethod.latin.settings.SpacingAndPunctuations; +import com.android.inputmethod.latin.suggestions.SuggestionStripViewAccessor; import com.android.inputmethod.latin.utils.AsyncResultHolder; import com.android.inputmethod.latin.utils.CollectionUtils; import com.android.inputmethod.latin.utils.InputTypeUtils; @@ -65,6 +67,7 @@ public final class InputLogic { // TODO : Remove this member when we can. private final LatinIME mLatinIME; + private final SuggestionStripViewAccessor mSuggestionStripViewAccessor; // Never null. private InputLogicHandler mInputLogicHandler = InputLogicHandler.NULL_HANDLER; @@ -94,8 +97,10 @@ public final class InputLogic { // Find a way to remove it for readability. public boolean mIsAutoCorrectionIndicatorOn; - public InputLogic(final LatinIME latinIME) { + public InputLogic(final LatinIME latinIME, + final SuggestionStripViewAccessor suggestionStripViewAccessor) { mLatinIME = latinIME; + mSuggestionStripViewAccessor = suggestionStripViewAccessor; mWordComposer = new WordComposer(); mEventInterpreter = new EventInterpreter(latinIME); mConnection = new RichInputConnection(latinIME); @@ -179,6 +184,108 @@ public final class InputLogic { } /** + * A suggestion was picked from the suggestion strip. + * @param settingsValues the current values of the settings. + * @param index the index of the suggestion. + * @param suggestionInfo the suggestion info. + */ + // Called from {@link SuggestionStripView} through the {@link SuggestionStripView#Listener} + // interface + public void onPickSuggestionManually(final SettingsValues settingsValues, + final int index, final SuggestedWordInfo suggestionInfo, + // TODO: remove these two arguments + final LatinIME.UIHandler handler, final KeyboardSwitcher keyboardSwitcher) { + 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(settingsValues)) { + // Word separators are suggested before the user inputs something. + // So, LatinImeLogger logs "" as a user's input. + LatinImeLogger.logOnManualSuggestion("", suggestion, index, suggestedWords); + // Rely on onCodeInput to do the complicated swapping/stripping logic consistently. + final int primaryCode = suggestion.charAt(0); + onCodeInput(primaryCode, + Constants.SUGGESTION_STRIP_COORDINATE, Constants.SUGGESTION_STRIP_COORDINATE, + settingsValues, handler, keyboardSwitcher); + if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { + ResearchLogger.latinIME_punctuationSuggestion(index, suggestion, + false /* isBatchMode */, suggestedWords.mIsPrediction); + } + return; + } + + mConnection.beginBatchEdit(); + if (SpaceState.PHANTOM == mSpaceState && suggestion.length() > 0 + // In the batch input mode, a manually picked suggested word should just replace + // the current batch input text and there is no need for a phantom space. + && !mWordComposer.isBatchMode()) { + final int firstChar = Character.codePointAt(suggestion, 0); + if (!settingsValues.isWordSeparator(firstChar) + || settingsValues.isUsuallyPrecededBySpace(firstChar)) { + promotePhantomSpace(settingsValues); + } + } + + // TODO: stop relying on mApplicationSpecifiedCompletions. The SuggestionInfo object + // should contain a reference to the CompletionInfo instead. + if (settingsValues.isApplicationSpecifiedCompletionsOn() + && mLatinIME.mApplicationSpecifiedCompletions != null + && index >= 0 && index < mLatinIME.mApplicationSpecifiedCompletions.length) { + mSuggestedWords = SuggestedWords.EMPTY; + mSuggestionStripViewAccessor.setNeutralSuggestionStrip(); + keyboardSwitcher.updateShiftState(); + resetComposingState(true /* alsoResetLastComposedWord */); + final CompletionInfo completionInfo = mLatinIME.mApplicationSpecifiedCompletions[index]; + mConnection.commitCompletion(completionInfo); + mConnection.endBatchEdit(); + return; + } + + // We need to log before we commit, because the word composer will store away the user + // typed word. + final String replacedWord = mWordComposer.getTypedWord(); + LatinImeLogger.logOnManualSuggestion(replacedWord, suggestion, index, suggestedWords); + commitChosenWord(settingsValues, suggestion, + LastComposedWord.COMMIT_TYPE_MANUAL_PICK, LastComposedWord.NOT_A_SEPARATOR); + if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { + ResearchLogger.latinIME_pickSuggestionManually(replacedWord, index, suggestion, + mWordComposer.isBatchMode(), suggestionInfo.mScore, + suggestionInfo.mKind, suggestionInfo.mSourceDict.mDictType); + } + mConnection.endBatchEdit(); + // Don't allow cancellation of manual pick + mLastComposedWord.deactivate(); + // Space state must be updated before calling updateShiftState + mSpaceState = SpaceState.PHANTOM; + keyboardSwitcher.updateShiftState(); + + // We should show the "Touch again to save" hint if the user pressed the first entry + // 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 Suggest suggest = mSuggest; + final boolean showingAddToDictionaryHint = + (SuggestedWordInfo.KIND_TYPED == suggestionInfo.mKind + || SuggestedWordInfo.KIND_OOV_CORRECTION == suggestionInfo.mKind) + && suggest != null + // If the suggestion is not in the dictionary, the hint should be shown. + && !suggest.mDictionaryFacilitator.isValidWord(suggestion, + true /* ignoreCase */); + + if (settingsValues.mIsInternal) { + LatinImeLoggerUtils.onSeparator((char)Constants.CODE_SPACE, + Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE); + } + if (showingAddToDictionaryHint + && suggest.mDictionaryFacilitator.isUserDictionaryEnabled()) { + mSuggestionStripViewAccessor.showAddToDictionaryHint(suggestion); + } else { + // If we're not showing the "Touch again to save", then update the suggestion strip. + handler.postUpdateSuggestionStrip(); + } + } + + /** * Consider an update to the cursor position. Evaluate whether this update has happened as * part of normal typing or whether it was an explicit cursor move by the user. In any case, * do the necessary adjustments. @@ -638,7 +745,7 @@ public final class InputLogic { mSpaceState = SpaceState.WEAK; } // In case the "add to dictionary" hint was still displayed. - mLatinIME.dismissAddToDictionaryHint(); + mSuggestionStripViewAccessor.dismissAddToDictionaryHint(); } handler.postUpdateSuggestionStrip(); if (settingsValues.mIsInternal) { @@ -714,7 +821,7 @@ public final class InputLogic { if (maybeDoubleSpacePeriod(settingsValues, handler)) { keyboardSwitcher.updateShiftState(); mSpaceState = SpaceState.DOUBLE; - } else if (!mLatinIME.isShowingPunctuationList()) { + } else if (!isShowingPunctuationList(settingsValues)) { mSpaceState = SpaceState.WEAK; } } @@ -745,7 +852,7 @@ public final class InputLogic { // Set punctuation right away. onUpdateSelection will fire but tests whether it is // already displayed or not, so it's okay. - mLatinIME.setNeutralSuggestionStrip(); + mSuggestionStripViewAccessor.setNeutralSuggestionStrip(); } keyboardSwitcher.updateShiftState(); @@ -1098,7 +1205,7 @@ public final class InputLogic { } if (!mWordComposer.isComposingWord() && !settingsValues.mBigramPredictionEnabled) { - mLatinIME.setNeutralSuggestionStrip(); + mSuggestionStripViewAccessor.setNeutralSuggestionStrip(); return; } @@ -1120,7 +1227,7 @@ public final class InputLogic { final SuggestedWords suggestedWords = holder.get(null, Constants.GET_SUGGESTED_WORDS_TIMEOUT); if (suggestedWords != null) { - mLatinIME.showSuggestionStrip(suggestedWords); + mSuggestionStripViewAccessor.showSuggestionStrip(suggestedWords); } } @@ -1326,6 +1433,15 @@ public final class InputLogic { } /** + * Find out if the punctuation list is shown in the suggestion strip. + * @return whether the current suggestions are the punctuation list. + */ + // TODO: make this private. It's used through LatinIME for tests. + public boolean isShowingPunctuationList(final SettingsValues settingsValues) { + return settingsValues.mSpacingAndPunctuations.mSuggestPuncList == mSuggestedWords; + } + + /** * Factor in auto-caps and manual caps and compute the current caps mode. * @param settingsValues the current settings values. * @param keyboardShiftMode the current shift mode of the keyboard. See @@ -1482,7 +1598,7 @@ public final class InputLogic { final int newSelStart, final int newSelEnd) { final boolean shouldFinishComposition = mWordComposer.isComposingWord(); resetComposingState(true /* alsoResetLastComposedWord */); - mLatinIME.setNeutralSuggestionStrip(); + mSuggestionStripViewAccessor.setNeutralSuggestionStrip(); mConnection.resetCachesUponCursorMoveAndReturnSuccess(newSelStart, newSelEnd, shouldFinishComposition); } @@ -1616,7 +1732,10 @@ public final class InputLogic { if (SpaceState.PHANTOM == mSpaceState) { promotePhantomSpace(settingsValues); } - if (settingsValues.mPhraseGestureEnabled) { + final SuggestedWordInfo autoCommitCandidate = mSuggestedWords.getAutoCommitCandidate(); + // Commit except the last word for phrase gesture if the top suggestion is eligible for auto + // commit. + if (settingsValues.mPhraseGestureEnabled && null != autoCommitCandidate) { // Find the last space final int indexOfLastSpace = batchInputText.lastIndexOf(Constants.CODE_SPACE) + 1; if (0 != indexOfLastSpace) { @@ -1720,9 +1839,8 @@ public final class InputLogic { // the segment of text starting at the supplied index and running for the length // of the auto-correction flash. At this moment, the "typedWord" argument is // ignored by TextView. - mConnection.commitCorrection( - new CorrectionInfo( - mConnection.getExpectedSelectionEnd() - typedWord.length(), + mConnection.commitCorrection(new CorrectionInfo( + mConnection.getExpectedSelectionEnd() - autoCorrection.length(), typedWord, autoCorrection)); } } diff --git a/java/src/com/android/inputmethod/latin/makedict/AbstractDictDecoder.java b/java/src/com/android/inputmethod/latin/makedict/AbstractDictDecoder.java index 1a9118147..5c7c4b8e3 100644 --- a/java/src/com/android/inputmethod/latin/makedict/AbstractDictDecoder.java +++ b/java/src/com/android/inputmethod/latin/makedict/AbstractDictDecoder.java @@ -19,7 +19,6 @@ package com.android.inputmethod.latin.makedict; import com.android.inputmethod.annotations.UsedForTesting; import com.android.inputmethod.latin.makedict.BinaryDictDecoderUtils.CharEncoding; import com.android.inputmethod.latin.makedict.BinaryDictDecoderUtils.DictBuffer; -import com.android.inputmethod.latin.makedict.FormatSpec.FileHeader; import com.android.inputmethod.latin.makedict.FormatSpec.FormatOptions; import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString; @@ -36,7 +35,7 @@ public abstract class AbstractDictDecoder implements DictDecoder { private static final int ERROR_CANNOT_READ = 1; private static final int ERROR_WRONG_FORMAT = 2; - protected FileHeader readHeader(final DictBuffer headerBuffer) + protected DictionaryHeader readHeader(final DictBuffer headerBuffer) throws IOException, UnsupportedFormatException { if (headerBuffer == null) { openDictBuffer(); @@ -57,10 +56,10 @@ public abstract class AbstractDictDecoder implements DictDecoder { final HashMap<String, String> attributes = HeaderReader.readAttributes(headerBuffer, headerSize); - final FileHeader header = new FileHeader(headerSize, + final DictionaryHeader header = new DictionaryHeader(headerSize, new FusionDictionary.DictionaryOptions(attributes), - new FormatOptions(version, FileHeader.ATTRIBUTE_VALUE_TRUE.equals( - attributes.get(FileHeader.HAS_HISTORICAL_INFO_KEY)))); + new FormatOptions(version, DictionaryHeader.ATTRIBUTE_VALUE_TRUE.equals( + attributes.get(DictionaryHeader.HAS_HISTORICAL_INFO_KEY)))); return header; } diff --git a/java/src/com/android/inputmethod/latin/makedict/BinaryDictDecoderUtils.java b/java/src/com/android/inputmethod/latin/makedict/BinaryDictDecoderUtils.java index 31747155e..369184573 100644 --- a/java/src/com/android/inputmethod/latin/makedict/BinaryDictDecoderUtils.java +++ b/java/src/com/android/inputmethod/latin/makedict/BinaryDictDecoderUtils.java @@ -17,7 +17,6 @@ package com.android.inputmethod.latin.makedict; import com.android.inputmethod.annotations.UsedForTesting; -import com.android.inputmethod.latin.makedict.FormatSpec.FileHeader; import com.android.inputmethod.latin.makedict.FormatSpec.FormatOptions; import com.android.inputmethod.latin.makedict.FusionDictionary.PtNode; import com.android.inputmethod.latin.makedict.FusionDictionary.PtNodeArray; @@ -598,7 +597,7 @@ public final class BinaryDictDecoderUtils { /* package */ static FusionDictionary readDictionaryBinary(final DictDecoder dictDecoder, final FusionDictionary dict) throws IOException, UnsupportedFormatException { // Read header - final FileHeader fileHeader = dictDecoder.readHeader(); + final DictionaryHeader fileHeader = dictDecoder.readHeader(); Map<Integer, PtNodeArray> reverseNodeArrayMapping = new TreeMap<Integer, PtNodeArray>(); Map<Integer, PtNode> reversePtNodeMapping = new TreeMap<Integer, PtNode>(); diff --git a/java/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtils.java b/java/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtils.java index 0dc50d14e..dea9f2e28 100644 --- a/java/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtils.java +++ b/java/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtils.java @@ -20,7 +20,6 @@ import com.android.inputmethod.annotations.UsedForTesting; import com.android.inputmethod.latin.Constants; import com.android.inputmethod.latin.makedict.BinaryDictDecoderUtils.CharEncoding; import com.android.inputmethod.latin.makedict.BinaryDictDecoderUtils.DictBuffer; -import com.android.inputmethod.latin.makedict.FormatSpec.FileHeader; import com.android.inputmethod.latin.makedict.FormatSpec.FormatOptions; import com.android.inputmethod.latin.makedict.FusionDictionary.PtNode; import com.android.inputmethod.latin.utils.ByteArrayDictBuffer; @@ -151,7 +150,7 @@ public final class BinaryDictIOUtils { final Map<Integer, ArrayList<PendingAttribute>> bigrams) throws IOException, UnsupportedFormatException { // Read header - final FileHeader header = dictDecoder.readHeader(); + final DictionaryHeader header = dictDecoder.readHeader(); readUnigramsAndBigramsBinaryInner(dictDecoder, header.mBodyOffset, words, frequencies, bigrams, header.mFormatOptions); } @@ -172,7 +171,7 @@ public final class BinaryDictIOUtils { if (word == null) return FormatSpec.NOT_VALID_WORD; dictDecoder.setPosition(0); - final FileHeader header = dictDecoder.readHeader(); + final DictionaryHeader header = dictDecoder.readHeader(); int wordPos = 0; final int wordLen = word.codePointCount(0, word.length()); for (int depth = 0; depth < Constants.DICTIONARY_MAX_WORD_LENGTH; ++depth) { @@ -311,7 +310,7 @@ public final class BinaryDictIOUtils { * @param length The length of the data file. * @return the header of the specified dictionary file. */ - private static FileHeader getDictionaryFileHeader( + private static DictionaryHeader getDictionaryFileHeader( final File file, final long offset, final long length) throws FileNotFoundException, IOException, UnsupportedFormatException { final byte[] buffer = new byte[HEADER_READING_BUFFER_SIZE]; @@ -337,10 +336,10 @@ public final class BinaryDictIOUtils { return dictDecoder.readHeader(); } - public static FileHeader getDictionaryFileHeaderOrNull(final File file, final long offset, + public static DictionaryHeader getDictionaryFileHeaderOrNull(final File file, final long offset, final long length) { try { - final FileHeader header = getDictionaryFileHeader(file, offset, length); + final DictionaryHeader header = getDictionaryFileHeader(file, offset, length); return header; } catch (UnsupportedFormatException e) { return null; diff --git a/java/src/com/android/inputmethod/latin/makedict/DictDecoder.java b/java/src/com/android/inputmethod/latin/makedict/DictDecoder.java index b4838f00f..bba1d434f 100644 --- a/java/src/com/android/inputmethod/latin/makedict/DictDecoder.java +++ b/java/src/com/android/inputmethod/latin/makedict/DictDecoder.java @@ -18,7 +18,6 @@ package com.android.inputmethod.latin.makedict; import com.android.inputmethod.annotations.UsedForTesting; import com.android.inputmethod.latin.makedict.BinaryDictDecoderUtils.DictBuffer; -import com.android.inputmethod.latin.makedict.FormatSpec.FileHeader; import com.android.inputmethod.latin.makedict.FormatSpec.FormatOptions; import com.android.inputmethod.latin.utils.ByteArrayDictBuffer; @@ -41,7 +40,7 @@ public interface DictDecoder { /** * Reads and returns the file header. */ - public FileHeader readHeader() throws IOException, UnsupportedFormatException; + public DictionaryHeader readHeader() throws IOException, UnsupportedFormatException; /** * Reads PtNode from ptNodePos. diff --git a/java/src/com/android/inputmethod/latin/makedict/DictionaryHeader.java b/java/src/com/android/inputmethod/latin/makedict/DictionaryHeader.java new file mode 100644 index 000000000..b99e281da --- /dev/null +++ b/java/src/com/android/inputmethod/latin/makedict/DictionaryHeader.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.latin.makedict; + +import com.android.inputmethod.latin.makedict.FormatSpec.FormatOptions; +import com.android.inputmethod.latin.makedict.FusionDictionary.DictionaryOptions; + +/** + * Class representing dictionary header. + */ +public final class DictionaryHeader { + public final int mBodyOffset; + public final DictionaryOptions mDictionaryOptions; + public final FormatOptions mFormatOptions; + + // Note that these are corresponding definitions in native code in latinime::HeaderPolicy + // and latinime::HeaderReadWriteUtils. + // TODO: Standardize the key names and bump up the format version, taking care not to + // break format version 2 dictionaries. + public static final String DICTIONARY_VERSION_KEY = "version"; + public static final String DICTIONARY_LOCALE_KEY = "locale"; + public static final String DICTIONARY_ID_KEY = "dictionary"; + public static final String DICTIONARY_DESCRIPTION_KEY = "description"; + public static final String DICTIONARY_DATE_KEY = "date"; + public static final String HAS_HISTORICAL_INFO_KEY = "HAS_HISTORICAL_INFO"; + public static final String USES_FORGETTING_CURVE_KEY = "USES_FORGETTING_CURVE"; + public static final String ATTRIBUTE_VALUE_TRUE = "1"; + + public DictionaryHeader(final int headerSize, final DictionaryOptions dictionaryOptions, + final FormatOptions formatOptions) throws UnsupportedFormatException { + mDictionaryOptions = dictionaryOptions; + mFormatOptions = formatOptions; + mBodyOffset = formatOptions.mVersion < FormatSpec.VERSION4 ? headerSize : 0; + if (null == getLocaleString()) { + throw new UnsupportedFormatException("Cannot create a FileHeader without a locale"); + } + if (null == getVersion()) { + throw new UnsupportedFormatException( + "Cannot create a FileHeader without a version"); + } + if (null == getId()) { + throw new UnsupportedFormatException("Cannot create a FileHeader without an ID"); + } + } + + // Helper method to get the locale as a String + public String getLocaleString() { + return mDictionaryOptions.mAttributes.get(DICTIONARY_LOCALE_KEY); + } + + // Helper method to get the version String + public String getVersion() { + return mDictionaryOptions.mAttributes.get(DICTIONARY_VERSION_KEY); + } + + // Helper method to get the dictionary ID as a String + public String getId() { + return mDictionaryOptions.mAttributes.get(DICTIONARY_ID_KEY); + } + + // Helper method to get the description + public String getDescription() { + // TODO: Right now each dictionary file comes with a description in its own language. + // It will display as is no matter the device's locale. It should be internationalized. + return mDictionaryOptions.mAttributes.get(DICTIONARY_DESCRIPTION_KEY); + } +}
\ No newline at end of file diff --git a/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java b/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java index 74e305976..5a3807389 100644 --- a/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java +++ b/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java @@ -19,7 +19,6 @@ package com.android.inputmethod.latin.makedict; import com.android.inputmethod.annotations.UsedForTesting; import com.android.inputmethod.latin.Constants; import com.android.inputmethod.latin.makedict.DictDecoder.DictionaryBufferFactory; -import com.android.inputmethod.latin.makedict.FusionDictionary.DictionaryOptions; import java.io.File; @@ -330,66 +329,6 @@ public final class FormatSpec { } /** - * Class representing file header. - */ - public static final class FileHeader { - public final int mBodyOffset; - public final DictionaryOptions mDictionaryOptions; - public final FormatOptions mFormatOptions; - - // Note that these are corresponding definitions in native code in latinime::HeaderPolicy - // and latinime::HeaderReadWriteUtils. - // TODO: Standardize the key names and bump up the format version, taking care not to - // break format version 2 dictionaries. - public static final String DICTIONARY_VERSION_KEY = "version"; - public static final String DICTIONARY_LOCALE_KEY = "locale"; - public static final String DICTIONARY_ID_KEY = "dictionary"; - public static final String DICTIONARY_DESCRIPTION_KEY = "description"; - public static final String DICTIONARY_DATE_KEY = "date"; - public static final String HAS_HISTORICAL_INFO_KEY = "HAS_HISTORICAL_INFO"; - public static final String USES_FORGETTING_CURVE_KEY = "USES_FORGETTING_CURVE"; - public static final String ATTRIBUTE_VALUE_TRUE = "1"; - public FileHeader(final int headerSize, final DictionaryOptions dictionaryOptions, - final FormatOptions formatOptions) throws UnsupportedFormatException { - mDictionaryOptions = dictionaryOptions; - mFormatOptions = formatOptions; - mBodyOffset = formatOptions.mVersion < VERSION4 ? headerSize : 0; - if (null == getLocaleString()) { - throw new UnsupportedFormatException("Cannot create a FileHeader without a locale"); - } - if (null == getVersion()) { - throw new UnsupportedFormatException( - "Cannot create a FileHeader without a version"); - } - if (null == getId()) { - throw new UnsupportedFormatException("Cannot create a FileHeader without an ID"); - } - } - - // Helper method to get the locale as a String - public String getLocaleString() { - return mDictionaryOptions.mAttributes.get(FileHeader.DICTIONARY_LOCALE_KEY); - } - - // Helper method to get the version String - public String getVersion() { - return mDictionaryOptions.mAttributes.get(FileHeader.DICTIONARY_VERSION_KEY); - } - - // Helper method to get the dictionary ID as a String - public String getId() { - return mDictionaryOptions.mAttributes.get(FileHeader.DICTIONARY_ID_KEY); - } - - // Helper method to get the description - public String getDescription() { - // TODO: Right now each dictionary file comes with a description in its own language. - // It will display as is no matter the device's locale. It should be internationalized. - return mDictionaryOptions.mAttributes.get(FileHeader.DICTIONARY_DESCRIPTION_KEY); - } - } - - /** * Returns new dictionary decoder. * * @param dictFile the dictionary file. diff --git a/java/src/com/android/inputmethod/latin/makedict/ProbabilityInfo.java b/java/src/com/android/inputmethod/latin/makedict/ProbabilityInfo.java new file mode 100644 index 000000000..c1a43cedf --- /dev/null +++ b/java/src/com/android/inputmethod/latin/makedict/ProbabilityInfo.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.latin.makedict; + +import com.android.inputmethod.latin.BinaryDictionary; + +public final class ProbabilityInfo { + public final int mProbability; + // mTimestamp, mLevel and mCount are historical info. These values are depend on the + // implementation in native code; thus, we must not use them and have any assumptions about + // them except for tests. + public final int mTimestamp; + public final int mLevel; + public final int mCount; + + public ProbabilityInfo(final int probability) { + this(probability, BinaryDictionary.NOT_A_VALID_TIMESTAMP, 0, 0); + } + + public ProbabilityInfo(final int probability, final int timestamp, final int level, + final int count) { + mProbability = probability; + mTimestamp = timestamp; + mLevel = level; + mCount = count; + } + + @Override + public String toString() { + return mTimestamp + ":" + mLevel + ":" + mCount; + } +}
\ No newline at end of file diff --git a/java/src/com/android/inputmethod/latin/makedict/Ver2DictDecoder.java b/java/src/com/android/inputmethod/latin/makedict/Ver2DictDecoder.java index ea0a2c6c2..d35f780cf 100644 --- a/java/src/com/android/inputmethod/latin/makedict/Ver2DictDecoder.java +++ b/java/src/com/android/inputmethod/latin/makedict/Ver2DictDecoder.java @@ -19,7 +19,6 @@ package com.android.inputmethod.latin.makedict; import com.android.inputmethod.annotations.UsedForTesting; import com.android.inputmethod.latin.makedict.BinaryDictDecoderUtils.CharEncoding; import com.android.inputmethod.latin.makedict.BinaryDictDecoderUtils.DictBuffer; -import com.android.inputmethod.latin.makedict.FormatSpec.FileHeader; import com.android.inputmethod.latin.makedict.FormatSpec.FormatOptions; import com.android.inputmethod.latin.makedict.FusionDictionary.PtNode; import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString; @@ -90,11 +89,11 @@ public class Ver2DictDecoder extends AbstractDictDecoder { } @Override - public FileHeader readHeader() throws IOException, UnsupportedFormatException { + public DictionaryHeader readHeader() throws IOException, UnsupportedFormatException { if (mDictBuffer == null) { openDictBuffer(); } - final FileHeader header = super.readHeader(mDictBuffer); + final DictionaryHeader header = super.readHeader(mDictBuffer); final int version = header.mFormatOptions.mVersion; if (!(version >= 2 && version <= 3)) { throw new UnsupportedFormatException("File header has a wrong version : " + version); diff --git a/java/src/com/android/inputmethod/latin/makedict/Ver4DictDecoder.java b/java/src/com/android/inputmethod/latin/makedict/Ver4DictDecoder.java index e459e4861..9ddaaf734 100644 --- a/java/src/com/android/inputmethod/latin/makedict/Ver4DictDecoder.java +++ b/java/src/com/android/inputmethod/latin/makedict/Ver4DictDecoder.java @@ -19,7 +19,6 @@ package com.android.inputmethod.latin.makedict; import com.android.inputmethod.annotations.UsedForTesting; import com.android.inputmethod.latin.makedict.BinaryDictDecoderUtils.CharEncoding; import com.android.inputmethod.latin.makedict.BinaryDictDecoderUtils.DictBuffer; -import com.android.inputmethod.latin.makedict.FormatSpec.FileHeader; import com.android.inputmethod.latin.makedict.FormatSpec.FormatOptions; import com.android.inputmethod.latin.makedict.FusionDictionary.PtNode; import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString; @@ -166,12 +165,12 @@ public class Ver4DictDecoder extends AbstractDictDecoder { } @Override - public FileHeader readHeader() throws IOException, UnsupportedFormatException { + public DictionaryHeader readHeader() throws IOException, UnsupportedFormatException { if (mHeaderBuffer == null) { openDictBuffer(); } mHeaderBuffer.position(0); - final FileHeader header = super.readHeader(mHeaderBuffer); + final DictionaryHeader header = super.readHeader(mHeaderBuffer); final int version = header.mFormatOptions.mVersion; if (version != FormatSpec.VERSION4) { throw new UnsupportedFormatException("File header has a wrong version : " + version); diff --git a/java/src/com/android/inputmethod/latin/makedict/Ver4DictEncoder.java b/java/src/com/android/inputmethod/latin/makedict/Ver4DictEncoder.java index b12f79b07..4b0efbad0 100644 --- a/java/src/com/android/inputmethod/latin/makedict/Ver4DictEncoder.java +++ b/java/src/com/android/inputmethod/latin/makedict/Ver4DictEncoder.java @@ -55,14 +55,16 @@ public class Ver4DictEncoder implements DictEncoder { throw new UnsupportedFormatException("Given path is not a directory."); } if (!BinaryDictionary.createEmptyDictFile(mDictPlacedDir.getAbsolutePath(), - FormatSpec.VERSION4, dict.mOptions.mAttributes)) { + FormatSpec.VERSION4, LocaleUtils.constructLocaleFromString( + dict.mOptions.mAttributes.get(DictionaryHeader.DICTIONARY_LOCALE_KEY)), + dict.mOptions.mAttributes)) { throw new IOException("Cannot create dictionary file : " + mDictPlacedDir.getAbsolutePath()); } final BinaryDictionary binaryDict = new BinaryDictionary(mDictPlacedDir.getAbsolutePath(), 0l, mDictPlacedDir.length(), true /* useFullEditDistance */, LocaleUtils.constructLocaleFromString(dict.mOptions.mAttributes.get( - FormatSpec.FileHeader.DICTIONARY_LOCALE_KEY)), + DictionaryHeader.DICTIONARY_LOCALE_KEY)), Dictionary.TYPE_USER /* Dictionary type. Does not matter for us */, true /* isUpdatable */); if (!binaryDict.isValidDictionary()) { diff --git a/java/src/com/android/inputmethod/latin/personalization/DecayingExpandableBinaryDictionaryBase.java b/java/src/com/android/inputmethod/latin/personalization/DecayingExpandableBinaryDictionaryBase.java index d636a253a..6a7a3368e 100644 --- a/java/src/com/android/inputmethod/latin/personalization/DecayingExpandableBinaryDictionaryBase.java +++ b/java/src/com/android/inputmethod/latin/personalization/DecayingExpandableBinaryDictionaryBase.java @@ -17,21 +17,15 @@ package com.android.inputmethod.latin.personalization; import android.content.Context; -import android.util.Log; import com.android.inputmethod.annotations.UsedForTesting; import com.android.inputmethod.latin.Constants; import com.android.inputmethod.latin.Dictionary; import com.android.inputmethod.latin.ExpandableBinaryDictionary; -import com.android.inputmethod.latin.makedict.DictDecoder; -import com.android.inputmethod.latin.makedict.FormatSpec; -import com.android.inputmethod.latin.makedict.UnsupportedFormatException; +import com.android.inputmethod.latin.makedict.DictionaryHeader; import com.android.inputmethod.latin.utils.LanguageModelParam; -import com.android.inputmethod.latin.utils.UserHistoryDictIOUtils; -import com.android.inputmethod.latin.utils.UserHistoryDictIOUtils.OnAddWordListener; import java.io.File; -import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.Locale; @@ -44,7 +38,6 @@ import java.util.concurrent.TimeUnit; */ public abstract class DecayingExpandableBinaryDictionaryBase extends ExpandableBinaryDictionary { private static final String TAG = DecayingExpandableBinaryDictionaryBase.class.getSimpleName(); - public static final boolean DBG_SAVE_RESTORE = false; private static final boolean DBG_DUMP_ON_CLOSE = false; /** Any pair being typed or picked */ @@ -53,8 +46,6 @@ public abstract class DecayingExpandableBinaryDictionaryBase extends ExpandableB public static final int FREQUENCY_FOR_WORDS_IN_DICTS = FREQUENCY_FOR_TYPED; public static final int FREQUENCY_FOR_WORDS_NOT_IN_DICTS = Dictionary.NOT_A_PROBABILITY; - public static final int REQUIRED_BINARY_DICTIONARY_VERSION = FormatSpec.VERSION4; - /** The locale for this dictionary. */ public final Locale mLocale; @@ -95,13 +86,13 @@ public abstract class DecayingExpandableBinaryDictionaryBase extends ExpandableB @Override protected Map<String, String> getHeaderAttributeMap() { HashMap<String, String> attributeMap = new HashMap<String, String>(); - attributeMap.put(FormatSpec.FileHeader.USES_FORGETTING_CURVE_KEY, - FormatSpec.FileHeader.ATTRIBUTE_VALUE_TRUE); - attributeMap.put(FormatSpec.FileHeader.HAS_HISTORICAL_INFO_KEY, - FormatSpec.FileHeader.ATTRIBUTE_VALUE_TRUE); - attributeMap.put(FormatSpec.FileHeader.DICTIONARY_ID_KEY, mDictName); - attributeMap.put(FormatSpec.FileHeader.DICTIONARY_LOCALE_KEY, mLocale.toString()); - attributeMap.put(FormatSpec.FileHeader.DICTIONARY_VERSION_KEY, + attributeMap.put(DictionaryHeader.USES_FORGETTING_CURVE_KEY, + DictionaryHeader.ATTRIBUTE_VALUE_TRUE); + attributeMap.put(DictionaryHeader.HAS_HISTORICAL_INFO_KEY, + DictionaryHeader.ATTRIBUTE_VALUE_TRUE); + attributeMap.put(DictionaryHeader.DICTIONARY_ID_KEY, mDictName); + attributeMap.put(DictionaryHeader.DICTIONARY_LOCALE_KEY, mLocale.toString()); + attributeMap.put(DictionaryHeader.DICTIONARY_VERSION_KEY, String.valueOf(TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()))); return attributeMap; } @@ -161,57 +152,6 @@ public abstract class DecayingExpandableBinaryDictionaryBase extends ExpandableB } @UsedForTesting - public void dumpAllWordsForDebug() { - runAfterGcForDebug(new Runnable() { - @Override - public void run() { - dumpAllWordsForDebugLocked(); - } - }); - } - - private void dumpAllWordsForDebugLocked() { - Log.d(TAG, "dumpAllWordsForDebug started."); - final OnAddWordListener listener = new OnAddWordListener() { - @Override - public void setUnigram(final String word, final String shortcutTarget, - final int frequency, final int shortcutFreq) { - Log.d(TAG, "load unigram: " + word + "," + frequency); - } - - @Override - public void setBigram(final String word0, final String word1, final int frequency) { - if (word0.length() < Constants.DICTIONARY_MAX_WORD_LENGTH - && word1.length() < Constants.DICTIONARY_MAX_WORD_LENGTH) { - Log.d(TAG, "load bigram: " + word0 + "," + word1 + "," + frequency); - } else { - Log.d(TAG, "Skip inserting a too long bigram: " + word0 + "," + word1 + "," - + frequency); - } - } - }; - - // Load the dictionary from binary file - final File dictFile = new File(mContext.getFilesDir(), mDictName); - final DictDecoder dictDecoder = FormatSpec.getDictDecoder(dictFile, - DictDecoder.USE_BYTEARRAY); - if (dictDecoder == null) { - // This is an expected condition: we don't have a user history dictionary for this - // language yet. It will be created sometime later. - return; - } - - try { - dictDecoder.openDictBuffer(); - UserHistoryDictIOUtils.readDictionaryBinary(dictDecoder, listener); - } catch (IOException e) { - Log.d(TAG, "IOException on opening a bytebuffer", e); - } catch (UnsupportedFormatException e) { - Log.d(TAG, "Unsupported format, can't read the dictionary", e); - } - } - - @UsedForTesting public void clearAndFlushDictionary() { // Clear the node structure on memory clear(); diff --git a/java/src/com/android/inputmethod/latin/personalization/UserHistoryDictionaryBigramList.java b/java/src/com/android/inputmethod/latin/personalization/UserHistoryDictionaryBigramList.java deleted file mode 100644 index 55a90ee51..000000000 --- a/java/src/com/android/inputmethod/latin/personalization/UserHistoryDictionaryBigramList.java +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Copyright (C) 2012 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.inputmethod.latin.personalization; - -import android.util.Log; - -import com.android.inputmethod.annotations.UsedForTesting; -import com.android.inputmethod.latin.utils.CollectionUtils; - -import java.util.HashMap; -import java.util.Set; - -/** - * A store of bigrams which will be updated when the user history dictionary is closed - * All bigrams including stale ones in SQL DB should be stored in this class to avoid adding stale - * bigrams when we write to the SQL DB. - */ -@UsedForTesting -public final class UserHistoryDictionaryBigramList { - public static final byte FORGETTING_CURVE_INITIAL_VALUE = 0; - private static final String TAG = UserHistoryDictionaryBigramList.class.getSimpleName(); - private static final HashMap<String, Byte> EMPTY_BIGRAM_MAP = CollectionUtils.newHashMap(); - private final HashMap<String, HashMap<String, Byte>> mBigramMap = CollectionUtils.newHashMap(); - private int mSize = 0; - - public void evictAll() { - mSize = 0; - mBigramMap.clear(); - } - - /** - * Called when the user typed a word. - */ - @UsedForTesting - public void addBigram(String word1, String word2) { - addBigram(word1, word2, FORGETTING_CURVE_INITIAL_VALUE); - } - - /** - * Called when loaded from the SQL DB. - */ - public void addBigram(String word1, String word2, byte fcValue) { - if (DecayingExpandableBinaryDictionaryBase.DBG_SAVE_RESTORE) { - Log.d(TAG, "--- add bigram: " + word1 + ", " + word2 + ", " + fcValue); - } - final HashMap<String, Byte> map; - if (mBigramMap.containsKey(word1)) { - map = mBigramMap.get(word1); - } else { - map = CollectionUtils.newHashMap(); - mBigramMap.put(word1, map); - } - if (!map.containsKey(word2)) { - ++mSize; - map.put(word2, fcValue); - } - } - - /** - * Called when inserted to the SQL DB. - */ - public void updateBigram(String word1, String word2, byte fcValue) { - if (DecayingExpandableBinaryDictionaryBase.DBG_SAVE_RESTORE) { - Log.d(TAG, "--- update bigram: " + word1 + ", " + word2 + ", " + fcValue); - } - final HashMap<String, Byte> map; - if (mBigramMap.containsKey(word1)) { - map = mBigramMap.get(word1); - } else { - return; - } - if (!map.containsKey(word2)) { - return; - } - map.put(word2, fcValue); - } - - public int size() { - return mSize; - } - - public boolean isEmpty() { - return mBigramMap.isEmpty(); - } - - public boolean containsKey(String word) { - return mBigramMap.containsKey(word); - } - - public Set<String> keySet() { - return mBigramMap.keySet(); - } - - public HashMap<String, Byte> getBigrams(String word1) { - if (mBigramMap.containsKey(word1)) return mBigramMap.get(word1); - // TODO: lower case according to locale - final String lowerWord1 = word1.toLowerCase(); - if (mBigramMap.containsKey(lowerWord1)) return mBigramMap.get(lowerWord1); - return EMPTY_BIGRAM_MAP; - } - - public boolean removeBigram(String word1, String word2) { - final HashMap<String, Byte> set = getBigrams(word1); - if (set.isEmpty()) { - return false; - } - if (set.containsKey(word2)) { - set.remove(word2); - --mSize; - return true; - } - return false; - } -} diff --git a/java/src/com/android/inputmethod/latin/settings/DebugSettings.java b/java/src/com/android/inputmethod/latin/settings/DebugSettings.java index 29bbed8bd..fa5ae92e7 100644 --- a/java/src/com/android/inputmethod/latin/settings/DebugSettings.java +++ b/java/src/com/android/inputmethod/latin/settings/DebugSettings.java @@ -16,15 +16,18 @@ package com.android.inputmethod.latin.settings; +import android.content.Intent; import android.content.SharedPreferences; import android.os.Bundle; import android.os.Process; import android.preference.CheckBoxPreference; import android.preference.Preference; +import android.preference.Preference.OnPreferenceClickListener; import android.preference.PreferenceFragment; import android.preference.PreferenceScreen; -import com.android.inputmethod.keyboard.KeyboardSwitcher; +import com.android.inputmethod.latin.Dictionary; +import com.android.inputmethod.latin.DictionaryDumpBroadcastReceiver; import com.android.inputmethod.latin.LatinImeLogger; import com.android.inputmethod.latin.R; import com.android.inputmethod.latin.debug.ExternalDictionaryGetterForDebug; @@ -40,6 +43,11 @@ public final class DebugSettings extends PreferenceFragment public static final String PREF_USE_ONLY_PERSONALIZATION_DICTIONARY_FOR_DEBUG = "use_only_personalization_dictionary_for_debug"; private static final String PREF_READ_EXTERNAL_DICTIONARY = "read_external_dictionary"; + private static final String PREF_DUMP_CONTACTS_DICT = "dump_contacts_dict"; + private static final String PREF_DUMP_USER_DICT = "dump_user_dict"; + private static final String PREF_DUMP_USER_HISTORY_DICT = "dump_user_history_dict"; + private static final String PREF_DUMP_PERSONALIZATION_DICT = "dump_personalization_dict"; + private static final boolean SHOW_STATISTICS_LOGGING = false; private boolean mServiceNeedsRestart = false; @@ -83,11 +91,53 @@ public final class DebugSettings extends PreferenceFragment }); } + final OnPreferenceClickListener dictDumpPrefClickListener = + new DictDumpPrefClickListener(this); + findPreference(PREF_DUMP_CONTACTS_DICT).setOnPreferenceClickListener( + dictDumpPrefClickListener); + findPreference(PREF_DUMP_USER_DICT).setOnPreferenceClickListener( + dictDumpPrefClickListener); + findPreference(PREF_DUMP_USER_HISTORY_DICT).setOnPreferenceClickListener( + dictDumpPrefClickListener); + findPreference(PREF_DUMP_PERSONALIZATION_DICT).setOnPreferenceClickListener( + dictDumpPrefClickListener); + mServiceNeedsRestart = false; mDebugMode = (CheckBoxPreference) findPreference(PREF_DEBUG_MODE); updateDebugMode(); } + private static class DictDumpPrefClickListener implements OnPreferenceClickListener { + final PreferenceFragment mPreferenceFragment; + + public DictDumpPrefClickListener(final PreferenceFragment preferenceFragment) { + mPreferenceFragment = preferenceFragment; + } + + @Override + public boolean onPreferenceClick(final Preference arg0) { + final String dictName; + if (arg0.getKey().equals(PREF_DUMP_CONTACTS_DICT)) { + dictName = Dictionary.TYPE_CONTACTS; + } else if (arg0.getKey().equals(PREF_DUMP_USER_DICT)) { + dictName = Dictionary.TYPE_USER; + } else if (arg0.getKey().equals(PREF_DUMP_USER_HISTORY_DICT)) { + dictName = Dictionary.TYPE_USER_HISTORY; + } else if (arg0.getKey().equals(PREF_DUMP_PERSONALIZATION_DICT)) { + dictName = Dictionary.TYPE_PERSONALIZATION; + } else { + dictName = null; + } + if (dictName != null) { + final Intent intent = + new Intent(DictionaryDumpBroadcastReceiver.DICTIONARY_DUMP_INTENT_ACTION); + intent.putExtra(DictionaryDumpBroadcastReceiver.DICTIONARY_NAME_KEY, dictName); + mPreferenceFragment.getActivity().sendBroadcast(intent); + } + return true; + } + } + @Override public void onStop() { super.onStop(); diff --git a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripViewAccessor.java b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripViewAccessor.java new file mode 100644 index 000000000..60f1c7a4e --- /dev/null +++ b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripViewAccessor.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.latin.suggestions; + +import com.android.inputmethod.latin.SuggestedWords; + +/** + * An object that gives basic control of a suggestion strip and some info on it. + */ +public interface SuggestionStripViewAccessor { + public boolean hasSuggestionStripView(); + public void showAddToDictionaryHint(final String word); + public boolean isShowingAddToDictionaryHint(); + public void dismissAddToDictionaryHint(); + public void setNeutralSuggestionStrip(); + public void showSuggestionStrip(final SuggestedWords suggestedWords); +} diff --git a/java/src/com/android/inputmethod/latin/utils/DictionaryInfoUtils.java b/java/src/com/android/inputmethod/latin/utils/DictionaryInfoUtils.java index 306735779..a15556511 100644 --- a/java/src/com/android/inputmethod/latin/utils/DictionaryInfoUtils.java +++ b/java/src/com/android/inputmethod/latin/utils/DictionaryInfoUtils.java @@ -29,7 +29,7 @@ import com.android.inputmethod.latin.BinaryDictionaryGetter; import com.android.inputmethod.latin.Constants; import com.android.inputmethod.latin.R; import com.android.inputmethod.latin.makedict.BinaryDictIOUtils; -import com.android.inputmethod.latin.makedict.FormatSpec.FileHeader; +import com.android.inputmethod.latin.makedict.DictionaryHeader; import com.android.inputmethod.latin.settings.SpacingAndPunctuations; import java.io.File; @@ -282,7 +282,7 @@ public class DictionaryInfoUtils { BinaryDictionaryGetter.ID_CATEGORY_SEPARATOR + locale.getLanguage().toString(); } - public static FileHeader getDictionaryFileHeaderOrNull(final File file) { + public static DictionaryHeader getDictionaryFileHeaderOrNull(final File file) { return BinaryDictIOUtils.getDictionaryFileHeaderOrNull(file, 0, file.length()); } @@ -294,7 +294,7 @@ public class DictionaryInfoUtils { */ private static DictionaryInfo createDictionaryInfoFromFileAddress( final AssetFileAddress fileAddress) { - final FileHeader header = BinaryDictIOUtils.getDictionaryFileHeaderOrNull( + final DictionaryHeader header = BinaryDictIOUtils.getDictionaryFileHeaderOrNull( new File(fileAddress.mFilename), fileAddress.mOffset, fileAddress.mLength); if (header == null) { return null; diff --git a/java/src/com/android/inputmethod/latin/utils/StringUtils.java b/java/src/com/android/inputmethod/latin/utils/StringUtils.java index c632a71a9..e7932b5a6 100644 --- a/java/src/com/android/inputmethod/latin/utils/StringUtils.java +++ b/java/src/com/android/inputmethod/latin/utils/StringUtils.java @@ -46,7 +46,7 @@ public final class StringUtils { public static String newSingleCodePointString(int codePoint) { if (Character.charCount(codePoint) == 1) { - // Optimization: avoid creating an temporary array for characters that are + // Optimization: avoid creating a temporary array for characters that are // represented by a single char value return String.valueOf((char) codePoint); } @@ -205,6 +205,24 @@ public final class StringUtils { return codePoints; } + /** + * Construct a String from a code point array + * + * @param codePoints a code point array that is null terminated when its logical length is + * shorter than the array length. + * @return a string constructed from the code point array. + */ + public static String getStringFromNullTerminatedCodePointArray(final int[] codePoints) { + int stringLength = codePoints.length; + for (int i = 0; i < codePoints.length; i++) { + if (codePoints[i] == 0) { + stringLength = i; + break; + } + } + return new String(codePoints, 0 /* offset */, stringLength); + } + // This method assumes the text is not null. For the empty string, it returns CAPITALIZE_NONE. public static int getCapitalizationType(final String text) { // If the first char is not uppercase, then the word is either all lower case or diff --git a/java/src/com/android/inputmethod/latin/utils/UserHistoryDictIOUtils.java b/java/src/com/android/inputmethod/latin/utils/UserHistoryDictIOUtils.java deleted file mode 100644 index 7af03da59..000000000 --- a/java/src/com/android/inputmethod/latin/utils/UserHistoryDictIOUtils.java +++ /dev/null @@ -1,181 +0,0 @@ -/* - * Copyright (C) 2012 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.inputmethod.latin.utils; - -import android.util.Log; - -import com.android.inputmethod.annotations.UsedForTesting; -import com.android.inputmethod.latin.makedict.BinaryDictIOUtils; -import com.android.inputmethod.latin.makedict.DictDecoder; -import com.android.inputmethod.latin.makedict.DictEncoder; -import com.android.inputmethod.latin.makedict.FormatSpec; -import com.android.inputmethod.latin.makedict.FormatSpec.FormatOptions; -import com.android.inputmethod.latin.makedict.FusionDictionary; -import com.android.inputmethod.latin.makedict.FusionDictionary.PtNodeArray; -import com.android.inputmethod.latin.makedict.PendingAttribute; -import com.android.inputmethod.latin.makedict.UnsupportedFormatException; -import com.android.inputmethod.latin.personalization.UserHistoryDictionaryBigramList; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Map.Entry; -import java.util.TreeMap; -import java.util.concurrent.TimeUnit; - -/** - * Reads and writes Binary files for a UserHistoryDictionary. - * - * All the methods in this class are static. - */ -public final class UserHistoryDictIOUtils { - private static final String TAG = UserHistoryDictIOUtils.class.getSimpleName(); - private static final boolean DEBUG = false; - - public interface OnAddWordListener { - /** - * Callback to be notified when a word is added to the dictionary. - * @param word The added word. - * @param shortcutTarget A shortcut target for this word, or null if none. - * @param frequency The frequency for this word. - * @param shortcutFreq The frequency of the shortcut (0~15, with 15 = whitelist). - * Unspecified if shortcutTarget is null - do not rely on its value. - */ - public void setUnigram(final String word, final String shortcutTarget, final int frequency, - final int shortcutFreq); - public void setBigram(final String word1, final String word2, final int frequency); - } - - @UsedForTesting - public interface BigramDictionaryInterface { - public int getFrequency(final String word1, final String word2); - } - - /** - * Writes dictionary to file. - */ - @UsedForTesting - public static void writeDictionary(final DictEncoder dictEncoder, - final BigramDictionaryInterface dict, final UserHistoryDictionaryBigramList bigrams, - final FormatOptions formatOptions, final HashMap<String, String> options) { - final FusionDictionary fusionDict = constructFusionDictionary(dict, bigrams, options); - fusionDict.addOptionAttribute(FormatSpec.FileHeader.USES_FORGETTING_CURVE_KEY, - FormatSpec.FileHeader.ATTRIBUTE_VALUE_TRUE); - fusionDict.addOptionAttribute(FormatSpec.FileHeader.DICTIONARY_DATE_KEY, - String.valueOf(TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()))); - try { - dictEncoder.writeDictionary(fusionDict, formatOptions); - Log.d(TAG, "end writing"); - } catch (IOException e) { - Log.e(TAG, "IO exception while writing file", e); - } catch (UnsupportedFormatException e) { - Log.e(TAG, "Unsupported format", e); - } - } - - /** - * Constructs a new FusionDictionary from BigramDictionaryInterface. - */ - @UsedForTesting - static FusionDictionary constructFusionDictionary(final BigramDictionaryInterface dict, - final UserHistoryDictionaryBigramList bigrams, final HashMap<String, String> options) { - final FusionDictionary fusionDict = new FusionDictionary(new PtNodeArray(), - new FusionDictionary.DictionaryOptions(options)); - int profTotal = 0; - for (final String word1 : bigrams.keySet()) { - final HashMap<String, Byte> word1Bigrams = bigrams.getBigrams(word1); - for (final String word2 : word1Bigrams.keySet()) { - final int freq = dict.getFrequency(word1, word2); - if (freq == -1) { - // don't add this bigram. - continue; - } - if (DEBUG) { - if (word1 == null) { - Log.d(TAG, "add unigram: " + word2 + "," + Integer.toString(freq)); - } else { - Log.d(TAG, "add bigram: " + word1 - + "," + word2 + "," + Integer.toString(freq)); - } - profTotal++; - } - if (word1 == null) { // unigram - fusionDict.add(word2, freq, null, false /* isNotAWord */); - } else { // bigram - if (FusionDictionary.findWordInTree(fusionDict.mRootNodeArray, word1) == null) { - fusionDict.add(word1, 2, null, false /* isNotAWord */); - } - fusionDict.setBigram(word1, word2, freq); - } - bigrams.updateBigram(word1, word2, (byte)freq); - } - } - if (DEBUG) { - Log.d(TAG, "add " + profTotal + "words"); - } - return fusionDict; - } - - /** - * Reads dictionary from file. - */ - public static void readDictionaryBinary(final DictDecoder dictDecoder, - final OnAddWordListener dict) { - final TreeMap<Integer, String> unigrams = CollectionUtils.newTreeMap(); - final TreeMap<Integer, Integer> frequencies = CollectionUtils.newTreeMap(); - final TreeMap<Integer, ArrayList<PendingAttribute>> bigrams = CollectionUtils.newTreeMap(); - try { - dictDecoder.readUnigramsAndBigramsBinary(unigrams, frequencies, bigrams); - } catch (IOException e) { - Log.e(TAG, "IO exception while reading file", e); - } catch (UnsupportedFormatException e) { - Log.e(TAG, "Unsupported format", e); - } catch (ArrayIndexOutOfBoundsException e) { - Log.e(TAG, "ArrayIndexOutOfBoundsException while reading file", e); - } - addWordsFromWordMap(unigrams, frequencies, bigrams, dict); - } - - /** - * Adds all unigrams and bigrams in maps to OnAddWordListener. - */ - @UsedForTesting - static void addWordsFromWordMap(final TreeMap<Integer, String> unigrams, - final TreeMap<Integer, Integer> frequencies, - final TreeMap<Integer, ArrayList<PendingAttribute>> bigrams, - final OnAddWordListener to) { - for (Entry<Integer, String> entry : unigrams.entrySet()) { - final String word1 = entry.getValue(); - final int unigramFrequency = frequencies.get(entry.getKey()); - to.setUnigram(word1, null /* shortcutTarget */, unigramFrequency, 0 /* shortcutFreq */); - final ArrayList<PendingAttribute> attrList = bigrams.get(entry.getKey()); - if (attrList != null) { - for (final PendingAttribute attr : attrList) { - 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, - BinaryDictIOUtils.reconstructBigramFrequency(unigramFrequency, - attr.mFrequency)); - } - } - } - - } -} diff --git a/java/src/com/android/inputmethod/latin/utils/WordProperty.java b/java/src/com/android/inputmethod/latin/utils/WordProperty.java index ba9b114b0..da56b213f 100644 --- a/java/src/com/android/inputmethod/latin/utils/WordProperty.java +++ b/java/src/com/android/inputmethod/latin/utils/WordProperty.java @@ -20,6 +20,7 @@ package com.android.inputmethod.latin.utils; import com.android.inputmethod.annotations.UsedForTesting; import com.android.inputmethod.latin.BinaryDictionary; import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString; +import com.android.inputmethod.latin.makedict.ProbabilityInfo; import java.util.ArrayList; @@ -37,32 +38,12 @@ public class WordProperty { public final ArrayList<ProbabilityInfo> mBigramProbabilityInfo = CollectionUtils.newArrayList(); public final ArrayList<WeightedString> mShortcutTargets = CollectionUtils.newArrayList(); - // TODO: Use this kind of Probability class for dictionary read/write code under the makedict - // package. - public static final class ProbabilityInfo { - public final int mProbability; - // wTimestamp, mLevel and mCount are historical info. These values are depend on the - // implementation in native code; thus, we must not use them and have any assumptions about - // them except for tests. - public final int mTimestamp; - public final int mLevel; - public final int mCount; - - public ProbabilityInfo(final int[] probabilityInfo) { - mProbability = probabilityInfo[BinaryDictionary.FORMAT_WORD_PROPERTY_PROBABILITY_INDEX]; - mTimestamp = probabilityInfo[BinaryDictionary.FORMAT_WORD_PROPERTY_TIMESTAMP_INDEX]; - mLevel = probabilityInfo[BinaryDictionary.FORMAT_WORD_PROPERTY_LEVEL_INDEX]; - mCount = probabilityInfo[BinaryDictionary.FORMAT_WORD_PROPERTY_COUNT_INDEX]; - } - } - - private static int getCodePointCount(final int[] codePoints) { - for (int i = 0; i < codePoints.length; i++) { - if (codePoints[i] == 0) { - return i; - } - } - return codePoints.length; + private static ProbabilityInfo createProbabilityInfoFromArray(final int[] probabilityInfo) { + return new ProbabilityInfo( + probabilityInfo[BinaryDictionary.FORMAT_WORD_PROPERTY_PROBABILITY_INDEX], + probabilityInfo[BinaryDictionary.FORMAT_WORD_PROPERTY_TIMESTAMP_INDEX], + probabilityInfo[BinaryDictionary.FORMAT_WORD_PROPERTY_LEVEL_INDEX], + probabilityInfo[BinaryDictionary.FORMAT_WORD_PROPERTY_COUNT_INDEX]); } // This represents invalid word when the probability is BinaryDictionary.NOT_A_PROBABILITY. @@ -72,20 +53,19 @@ public class WordProperty { final ArrayList<int[]> bigramTargets, final ArrayList<int[]> bigramProbabilityInfo, final ArrayList<int[]> shortcutTargets, final ArrayList<Integer> shortcutProbabilities) { - mCodePoints = new String(codePoints, 0 /* offset */, getCodePointCount(codePoints)); + mCodePoints = StringUtils.getStringFromNullTerminatedCodePointArray(codePoints); mIsNotAWord = isNotAWord; mIsBlacklisted = isBlacklisted; mHasBigrams = hasBigram; mHasShortcuts = hasShortcuts; - mProbabilityInfo = new ProbabilityInfo(probabilityInfo); + mProbabilityInfo = createProbabilityInfoFromArray(probabilityInfo); final int bigramTargetCount = bigramTargets.size(); for (int i = 0; i < bigramTargetCount; i++) { - final int[] bigramTargetCodePointArray = bigramTargets.get(i); - final String bigramTargetString = new String(bigramTargetCodePointArray, - 0 /* offset */, getCodePointCount(bigramTargetCodePointArray)); + final String bigramTargetString = + StringUtils.getStringFromNullTerminatedCodePointArray(bigramTargets.get(i)); final ProbabilityInfo bigramProbability = - new ProbabilityInfo(bigramProbabilityInfo.get(i)); + createProbabilityInfoFromArray(bigramProbabilityInfo.get(i)); mBigramTargets.add( new WeightedString(bigramTargetString, bigramProbability.mProbability)); mBigramProbabilityInfo.add(bigramProbability); @@ -93,9 +73,8 @@ public class WordProperty { final int shortcutTargetCount = shortcutTargets.size(); for (int i = 0; i < shortcutTargetCount; i++) { - final int[] shortcutTargetCodePointArray = shortcutTargets.get(i); - final String shortcutTargetString = new String(shortcutTargetCodePointArray, - 0 /* offset */, getCodePointCount(shortcutTargetCodePointArray)); + final String shortcutTargetString = + StringUtils.getStringFromNullTerminatedCodePointArray(shortcutTargets.get(i)); mShortcutTargets.add( new WeightedString(shortcutTargetString, shortcutProbabilities.get(i))); } @@ -105,4 +84,44 @@ public class WordProperty { public boolean isValid() { return mProbabilityInfo.mProbability != BinaryDictionary.NOT_A_PROBABILITY; } + + @Override + public String toString() { + // TODO: Move this logic to CombinedInputOutput. + final StringBuffer builder = new StringBuffer(); + builder.append(" word=" + mCodePoints); + builder.append(","); + builder.append("f=" + mProbabilityInfo.mProbability); + if (mIsNotAWord) { + builder.append(","); + builder.append("not_a_word=true"); + } + if (mIsBlacklisted) { + builder.append(","); + builder.append("blacklisted=true"); + } + if (mProbabilityInfo.mTimestamp != BinaryDictionary.NOT_A_VALID_TIMESTAMP) { + builder.append(","); + builder.append("historicalInfo=" + mProbabilityInfo); + } + builder.append("\n"); + for (int i = 0; i < mBigramTargets.size(); i++) { + builder.append(" bigram=" + mBigramTargets.get(i).mWord); + builder.append(","); + builder.append("f=" + mBigramTargets.get(i).mFrequency); + if (mBigramProbabilityInfo.get(i).mTimestamp + != BinaryDictionary.NOT_A_VALID_TIMESTAMP) { + builder.append(","); + builder.append("historicalInfo=" + mBigramProbabilityInfo.get(i)); + } + builder.append("\n"); + } + for (int i = 0; i < mShortcutTargets.size(); i++) { + builder.append(" shortcut=" + mShortcutTargets.get(i).mWord); + builder.append(","); + builder.append("f=" + mShortcutTargets.get(i).mFrequency); + builder.append("\n"); + } + return builder.toString(); + } }
\ No newline at end of file |