1/*
2 * Copyright (C) 2011 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5 * use this file except in compliance with the License. You may obtain a copy of
6 * the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 * License for the specific language governing permissions and limitations under
14 * the License.
15 */
16
17package com.android.inputmethod.keyboard;
18
19import android.graphics.Rect;
20import android.text.TextUtils;
21
22import com.android.inputmethod.keyboard.internal.TouchPositionCorrection;
23import com.android.inputmethod.latin.Constants;
24import com.android.inputmethod.latin.JniUtils;
25
26import java.util.Arrays;
27
28public final class ProximityInfo {
29    /** MAX_PROXIMITY_CHARS_SIZE must be the same as MAX_PROXIMITY_CHARS_SIZE_INTERNAL
30     * in defines.h */
31    public static final int MAX_PROXIMITY_CHARS_SIZE = 16;
32    /** Number of key widths from current touch point to search for nearest keys. */
33    private static float SEARCH_DISTANCE = 1.2f;
34    private static final Key[] EMPTY_KEY_ARRAY = new Key[0];
35    private static final float DEFAULT_TOUCH_POSITION_CORRECTION_RADIUS = 0.15f;
36
37    private final int mGridWidth;
38    private final int mGridHeight;
39    private final int mGridSize;
40    private final int mCellWidth;
41    private final int mCellHeight;
42    // TODO: Find a proper name for mKeyboardMinWidth
43    private final int mKeyboardMinWidth;
44    private final int mKeyboardHeight;
45    private final int mMostCommonKeyWidth;
46    private final int mMostCommonKeyHeight;
47    private final Key[] mKeys;
48    private final Key[][] mGridNeighbors;
49    private final String mLocaleStr;
50
51    ProximityInfo(final String localeStr, final int gridWidth, final int gridHeight,
52            final int minWidth, final int height, final int mostCommonKeyWidth,
53            final int mostCommonKeyHeight, final Key[] keys,
54            final TouchPositionCorrection touchPositionCorrection) {
55        if (TextUtils.isEmpty(localeStr)) {
56            mLocaleStr = "";
57        } else {
58            mLocaleStr = localeStr;
59        }
60        mGridWidth = gridWidth;
61        mGridHeight = gridHeight;
62        mGridSize = mGridWidth * mGridHeight;
63        mCellWidth = (minWidth + mGridWidth - 1) / mGridWidth;
64        mCellHeight = (height + mGridHeight - 1) / mGridHeight;
65        mKeyboardMinWidth = minWidth;
66        mKeyboardHeight = height;
67        mMostCommonKeyHeight = mostCommonKeyHeight;
68        mMostCommonKeyWidth = mostCommonKeyWidth;
69        mKeys = keys;
70        mGridNeighbors = new Key[mGridSize][];
71        if (minWidth == 0 || height == 0) {
72            // No proximity required. Keyboard might be more keys keyboard.
73            return;
74        }
75        computeNearestNeighbors();
76        mNativeProximityInfo = createNativeProximityInfo(touchPositionCorrection);
77    }
78
79    public static ProximityInfo createDummyProximityInfo() {
80        return new ProximityInfo("", 1, 1, 1, 1, 1, 1, EMPTY_KEY_ARRAY, null);
81    }
82
83    public static ProximityInfo createSpellCheckerProximityInfo(final int[] proximity,
84            final int rowSize, final int gridWidth, final int gridHeight) {
85        final ProximityInfo spellCheckerProximityInfo = createDummyProximityInfo();
86        spellCheckerProximityInfo.mNativeProximityInfo =
87                spellCheckerProximityInfo.setProximityInfoNative("",
88                        rowSize, gridWidth, gridHeight, gridWidth, gridHeight,
89                        1, proximity, 0, null, null, null, null, null, null, null, null);
90        return spellCheckerProximityInfo;
91    }
92
93    private long mNativeProximityInfo;
94    static {
95        JniUtils.loadNativeLibrary();
96    }
97
98    private native long setProximityInfoNative(
99            String locale, int maxProximityCharsSize, int displayWidth,
100            int displayHeight, int gridWidth, int gridHeight,
101            int mostCommonKeyWidth, int[] proximityCharsArray,
102            int keyCount, int[] keyXCoordinates, int[] keyYCoordinates,
103            int[] keyWidths, int[] keyHeights, int[] keyCharCodes,
104            float[] sweetSpotCenterX, float[] sweetSpotCenterY, float[] sweetSpotRadii);
105
106    private native void releaseProximityInfoNative(long nativeProximityInfo);
107
108    private final long createNativeProximityInfo(
109            final TouchPositionCorrection touchPositionCorrection) {
110        final Key[][] gridNeighborKeys = mGridNeighbors;
111        final int keyboardWidth = mKeyboardMinWidth;
112        final int keyboardHeight = mKeyboardHeight;
113        final Key[] keys = mKeys;
114        final int[] proximityCharsArray = new int[mGridSize * MAX_PROXIMITY_CHARS_SIZE];
115        Arrays.fill(proximityCharsArray, Constants.NOT_A_CODE);
116        for (int i = 0; i < mGridSize; ++i) {
117            final int proximityCharsLength = gridNeighborKeys[i].length;
118            for (int j = 0; j < proximityCharsLength; ++j) {
119                proximityCharsArray[i * MAX_PROXIMITY_CHARS_SIZE + j] =
120                        gridNeighborKeys[i][j].mCode;
121            }
122        }
123        final int keyCount = keys.length;
124        final int[] keyXCoordinates = new int[keyCount];
125        final int[] keyYCoordinates = new int[keyCount];
126        final int[] keyWidths = new int[keyCount];
127        final int[] keyHeights = new int[keyCount];
128        final int[] keyCharCodes = new int[keyCount];
129        final float[] sweetSpotCenterXs;
130        final float[] sweetSpotCenterYs;
131        final float[] sweetSpotRadii;
132
133        for (int i = 0; i < keyCount; ++i) {
134            final Key key = keys[i];
135            keyXCoordinates[i] = key.mX;
136            keyYCoordinates[i] = key.mY;
137            keyWidths[i] = key.mWidth;
138            keyHeights[i] = key.mHeight;
139            keyCharCodes[i] = key.mCode;
140        }
141
142        if (touchPositionCorrection != null && touchPositionCorrection.isValid()) {
143            sweetSpotCenterXs = new float[keyCount];
144            sweetSpotCenterYs = new float[keyCount];
145            sweetSpotRadii = new float[keyCount];
146            final float defaultRadius = DEFAULT_TOUCH_POSITION_CORRECTION_RADIUS
147                    * (float)Math.hypot(mMostCommonKeyWidth, mMostCommonKeyHeight);
148            for (int i = 0; i < keyCount; i++) {
149                final Key key = keys[i];
150                final Rect hitBox = key.mHitBox;
151                sweetSpotCenterXs[i] = hitBox.exactCenterX();
152                sweetSpotCenterYs[i] = hitBox.exactCenterY();
153                sweetSpotRadii[i] = defaultRadius;
154                final int row = hitBox.top / mMostCommonKeyHeight;
155                if (row < touchPositionCorrection.getRows()) {
156                    final int hitBoxWidth = hitBox.width();
157                    final int hitBoxHeight = hitBox.height();
158                    final float hitBoxDiagonal = (float)Math.hypot(hitBoxWidth, hitBoxHeight);
159                    sweetSpotCenterXs[i] += touchPositionCorrection.getX(row) * hitBoxWidth;
160                    sweetSpotCenterYs[i] += touchPositionCorrection.getY(row) * hitBoxHeight;
161                    sweetSpotRadii[i] = touchPositionCorrection.getRadius(row) * hitBoxDiagonal;
162                }
163            }
164        } else {
165            sweetSpotCenterXs = sweetSpotCenterYs = sweetSpotRadii = null;
166        }
167
168        return setProximityInfoNative(mLocaleStr, MAX_PROXIMITY_CHARS_SIZE,
169                keyboardWidth, keyboardHeight, mGridWidth, mGridHeight, mMostCommonKeyWidth,
170                proximityCharsArray,
171                keyCount, keyXCoordinates, keyYCoordinates, keyWidths, keyHeights, keyCharCodes,
172                sweetSpotCenterXs, sweetSpotCenterYs, sweetSpotRadii);
173    }
174
175    public long getNativeProximityInfo() {
176        return mNativeProximityInfo;
177    }
178
179    @Override
180    protected void finalize() throws Throwable {
181        try {
182            if (mNativeProximityInfo != 0) {
183                releaseProximityInfoNative(mNativeProximityInfo);
184                mNativeProximityInfo = 0;
185            }
186        } finally {
187            super.finalize();
188        }
189    }
190
191    private void computeNearestNeighbors() {
192        final int defaultWidth = mMostCommonKeyWidth;
193        final Key[] keys = mKeys;
194        final int thresholdBase = (int) (defaultWidth * SEARCH_DISTANCE);
195        final int threshold = thresholdBase * thresholdBase;
196        // Round-up so we don't have any pixels outside the grid
197        final Key[] neighborKeys = new Key[keys.length];
198        final int gridWidth = mGridWidth * mCellWidth;
199        final int gridHeight = mGridHeight * mCellHeight;
200        for (int x = 0; x < gridWidth; x += mCellWidth) {
201            for (int y = 0; y < gridHeight; y += mCellHeight) {
202                final int centerX = x + mCellWidth / 2;
203                final int centerY = y + mCellHeight / 2;
204                int count = 0;
205                for (final Key key : keys) {
206                    if (key.isSpacer()) continue;
207                    if (key.squaredDistanceToEdge(centerX, centerY) < threshold) {
208                        neighborKeys[count++] = key;
209                    }
210                }
211                mGridNeighbors[(y / mCellHeight) * mGridWidth + (x / mCellWidth)] =
212                        Arrays.copyOfRange(neighborKeys, 0, count);
213            }
214        }
215    }
216
217    public void fillArrayWithNearestKeyCodes(final int x, final int y, final int primaryKeyCode,
218            final int[] dest) {
219        final int destLength = dest.length;
220        if (destLength < 1) {
221            return;
222        }
223        int index = 0;
224        if (primaryKeyCode > Keyboard.CODE_SPACE) {
225            dest[index++] = primaryKeyCode;
226        }
227        final Key[] nearestKeys = getNearestKeys(x, y);
228        for (Key key : nearestKeys) {
229            if (index >= destLength) {
230                break;
231            }
232            final int code = key.mCode;
233            if (code <= Keyboard.CODE_SPACE) {
234                break;
235            }
236            dest[index++] = code;
237        }
238        if (index < destLength) {
239            dest[index] = Constants.NOT_A_CODE;
240        }
241    }
242
243    public Key[] getNearestKeys(final int x, final int y) {
244        if (mGridNeighbors == null) {
245            return EMPTY_KEY_ARRAY;
246        }
247        if (x >= 0 && x < mKeyboardMinWidth && y >= 0 && y < mKeyboardHeight) {
248            int index = (y / mCellHeight) * mGridWidth + (x / mCellWidth);
249            if (index < mGridSize) {
250                return mGridNeighbors[index];
251            }
252        }
253        return EMPTY_KEY_ARRAY;
254    }
255}
256